Objects in Godot 4
Built-in Object Types
# ============================================
# OBJECT - Base class for everything
# ============================================
# Object is the root class that everything inherits from
# You rarely use Object directly
# ============================================
# REFCOUNTED - Memory-managed objects
# ============================================
# Automatically deleted when no longer referenced
# Use for data containers and logic classes
class_name InventorySystem extends RefCounted
var items: Array[Item] = []
var max_slots: int = 20
func add_item(item: Item) -> bool:
if items.size() < max_slots:
items.append(item)
return true
return false
# Usage:
var inventory = InventorySystem.new() # Created
inventory.add_item(sword)
inventory = null # Automatically freed when no references exist
# ============================================
# NODE - Scene tree objects
# ============================================
# Part of the scene tree, has lifecycle methods
# Can have parent/children relationships
class_name GameController extends Node
func _ready():
print("Node entered scene tree")
func _process(delta):
# Called every frame
pass
# ============================================
# RESOURCE - Serializable data
# ============================================
# Can be saved to .tres files
# Shared between instances by default
class_name ItemData extends Resource
@export var item_name: String
@export var icon: Texture2D
@export var value: int
# Save as item_sword.tres and reuse everywhere
Object vs RefCounted vs Node
| Type |
Memory |
Scene Tree |
Lifecycle |
Best For |
Object |
Manual |
No |
Manual |
Rarely used directly |
RefCounted |
Auto |
No |
Reference counting |
Data, logic, utilities |
Node |
Manual* |
Yes |
_ready, _process, etc |
Game objects, scenes |
Resource |
Auto |
No |
Reference counting |
Reusable data templates |
*Nodes are freed when removed from tree or parent is freed
Creating Custom Objects
Custom Data Object (RefCounted)
# ============================================
# USE CASE: Pure data container
# ============================================
class_name QuestData extends RefCounted
var quest_id: String
var title: String
var description: String
var objectives: Array[String] = []
var is_completed: bool = false
var reward_gold: int = 0
func _init(id: String, quest_title: String):
quest_id = id
title = quest_title
func complete() -> void:
is_completed = true
print("Quest completed: ", title)
func add_objective(objective: String) -> void:
objectives.append(objective)
# ADVANTAGE: Lightweight, no scene tree overhead
# USAGE: When you need structured data without visuals
# Example:
var quest = QuestData.new("quest_001", "Save the Village")
quest.add_objective("Defeat 10 goblins")
quest.add_objective("Return to elder")
quest.reward_gold = 100
Custom Logic Object (RefCounted)
# ============================================
# USE CASE: Reusable logic/algorithms
# ============================================
class_name PathfindingHelper extends RefCounted
func find_path(start: Vector2, end: Vector2, obstacles: Array) -> Array[Vector2]:
# Pathfinding algorithm here
var path: Array[Vector2] = []
# ... calculate path
return path
func get_nearest_point(position: Vector2, points: Array[Vector2]) -> Vector2:
var nearest = points[0]
var min_distance = position.distance_to(nearest)
for point in points:
var distance = position.distance_to(point)
if distance < min_distance:
min_distance = distance
nearest = point
return nearest
# ADVANTAGE: No scene tree dependency, easy to test
# USAGE: Utilities, algorithms, math helpers
# Example:
var pathfinder = PathfindingHelper.new()
var path = pathfinder.find_path(player_pos, enemy_pos, walls)
Custom State Machine (RefCounted)
# ============================================
# USE CASE: State management without Node overhead
# ============================================
class_name StateMachine extends RefCounted
signal state_changed(old_state: String, new_state: String)
var current_state: String = ""
var states: Dictionary = {}
func add_state(state_name: String, enter_func: Callable, exit_func: Callable) -> void:
states[state_name] = {
"enter": enter_func,
"exit": exit_func
}
func change_state(new_state: String) -> void:
if not states.has(new_state):
push_error("State doesn't exist: " + new_state)
return
# Exit current state
if current_state != "" and states[current_state].has("exit"):
states[current_state]["exit"].call()
var old_state = current_state
current_state = new_state
# Enter new state
if states[new_state].has("enter"):
states[new_state]["enter"].call()
state_changed.emit(old_state, new_state)
# ADVANTAGE: Lightweight, reusable across different nodes
# USAGE: AI states, animation states, game states
# Example:
class_name Enemy extends CharacterBody2D
var state_machine: StateMachine
func _ready():
state_machine = StateMachine.new()
state_machine.add_state("idle", _enter_idle, _exit_idle)
state_machine.add_state("chase", _enter_chase, _exit_chase)
state_machine.add_state("attack", _enter_attack, _exit_attack)
state_machine.change_state("idle")
func _enter_idle(): print("Now idle")
func _exit_idle(): print("Leaving idle")
func _enter_chase(): print("Now chasing")
func _exit_chase(): print("Stop chasing")
Custom Resource Object
# ============================================
# USE CASE: Reusable data templates
# ============================================
class_name EnemyStats extends Resource
@export_group("Basic Info")
@export var enemy_name: String = "Goblin"
@export var sprite: Texture2D
@export_group("Stats")
@export var max_health: int = 50
@export var move_speed: float = 100.0
@export var damage: int = 10
@export_group("Drops")
@export var gold_drop: int = 25
@export var experience: int = 50
# ADVANTAGE: Create .tres files, share between enemies
# USAGE: When you need multiple instances with different values
# Create goblin_stats.tres, orc_stats.tres, etc.
# Then in enemy script:
class_name Enemy extends CharacterBody2D
@export var stats: EnemyStats
var current_health: int
func _ready():
if stats:
current_health = stats.max_health
$Sprite2D.texture = stats.sprite
Use Cases for Custom Objects
1. Inventory System
# ============================================
# INVENTORY SYSTEM - RefCounted
# ============================================
class_name Inventory extends RefCounted
signal item_added(item: Item)
signal item_removed(item: Item)
var items: Array[Item] = []
var max_capacity: int = 20
func add_item(item: Item) -> bool:
if items.size() >= max_capacity:
return false
items.append(item)
item_added.emit(item)
return true
func remove_item(item: Item) -> bool:
var index = items.find(item)
if index != -1:
items.remove_at(index)
item_removed.emit(item)
return true
return false
func has_item(item_name: String) -> bool:
for item in items:
if item.item_name == item_name:
return true
return false
func get_total_weight() -> float:
var weight = 0.0
for item in items:
weight += item.weight
return weight
# WHY CUSTOM: Encapsulates inventory logic, easy to test
2. Dialogue System
# ============================================
# DIALOGUE DATA - Resource
# ============================================
class_name DialogueData extends Resource
@export var lines: Array[String] = []
@export var speaker_name: String = ""
@export var portrait: Texture2D
# ============================================
# DIALOGUE MANAGER - RefCounted
# ============================================
class_name DialogueManager extends RefCounted
signal dialogue_started(data: DialogueData)
signal line_changed(line: String, index: int)
signal dialogue_ended
var current_dialogue: DialogueData
var current_line_index: int = 0
func start_dialogue(data: DialogueData) -> void:
current_dialogue = data
current_line_index = 0
dialogue_started.emit(data)
line_changed.emit(data.lines[0], 0)
func next_line() -> bool:
current_line_index += 1
if current_line_index >= current_dialogue.lines.size():
dialogue_ended.emit()
return false
line_changed.emit(current_dialogue.lines[current_line_index], current_line_index)
return true
# WHY CUSTOM: Separates dialogue data from UI logic
3. Save/Load System
# ============================================
# SAVE DATA - RefCounted
# ============================================
class_name SaveData extends RefCounted
var player_position: Vector2
var player_health: int
var player_gold: int
var inventory_items: Array[String] = []
var completed_quests: Array[String] = []
var current_level: String
func to_dict() -> Dictionary:
return {
"player_position": {"x": player_position.x, "y": player_position.y},
"player_health": player_health,
"player_gold": player_gold,
"inventory_items": inventory_items,
"completed_quests": completed_quests,
"current_level": current_level
}
func from_dict(data: Dictionary) -> void:
player_position = Vector2(data.player_position.x, data.player_position.y)
player_health = data.player_health
player_gold = data.player_gold
inventory_items = data.inventory_items
completed_quests = data.completed_quests
current_level = data.current_level
# ============================================
# SAVE MANAGER - RefCounted
# ============================================
class_name SaveManager extends RefCounted
const SAVE_PATH = "user://savegame.json"
func save_game(save_data: SaveData) -> void:
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
var json = JSON.stringify(save_data.to_dict())
file.store_string(json)
file.close()
func load_game() -> SaveData:
if not FileAccess.file_exists(SAVE_PATH):
return null
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
var json = file.get_as_text()
file.close()
var data = JSON.parse_string(json)
var save_data = SaveData.new()
save_data.from_dict(data)
return save_data
# WHY CUSTOM: Centralized save logic, easy to modify format
4. Damage Calculator
# ============================================
# DAMAGE CALCULATOR - RefCounted
# ============================================
class_name DamageCalculator extends RefCounted
enum DamageType { PHYSICAL, FIRE, ICE, POISON }
func calculate_damage(base_damage: int, attacker_stats: Dictionary, defender_stats: Dictionary) -> int:
var damage = base_damage
# Apply attack modifier
if attacker_stats.has("attack"):
damage += attacker_stats.attack * 0.5
# Apply defense reduction
if defender_stats.has("defense"):
damage -= defender_stats.defense * 0.3
# Random variance
damage *= randf_range(0.9, 1.1)
return max(1, int(damage))
func calculate_critical(damage: int, crit_chance: float, crit_multiplier: float) -> int:
if randf() < crit_chance:
return int(damage * crit_multiplier)
return damage
func apply_resistance(damage: int, damage_type: DamageType, resistances: Dictionary) -> int:
if resistances.has(damage_type):
var resistance = resistances[damage_type]
damage *= (1.0 - resistance)
return int(damage)
# WHY CUSTOM: Complex calculations isolated, easy to balance/test
Advantages of Custom Objects
1. Separation of Concerns
# Without custom objects - everything in one script
extends CharacterBody2D
# 500+ lines of movement, inventory, combat, quests, etc.
# With custom objects - organized
extends CharacterBody2D
var inventory: Inventory
var quest_log: QuestLog
var combat_stats: CombatStats
# Each system is separate and manageable
2. Reusability
# Create once, use everywhere
var player_inventory = Inventory.new(20) # 20 slots
var chest_inventory = Inventory.new(10) # 10 slots
var shop_inventory = Inventory.new(50) # 50 slots
3. Testability
# Easy to test without running entire game
var inventory = Inventory.new()
assert(inventory.add_item(sword) == true)
assert(inventory.items.size() == 1)
4. Memory Efficiency
# Resource shared between instances
@export var enemy_stats: EnemyStats # Points to same .tres file
# All goblins share goblin_stats.tres - one copy in memory
5. Type Safety
# Without custom object
var player_data = {
"name": "Hero",
"health": 100,
"glod": 50 # Typo! Hard to find
}
# With custom object
var player_data = PlayerData.new()
player_data.glod = 50 # Compile error - catches typo
When to Create Custom Objects
| Create Custom Object When... |
Example |
| Data needs structure |
PlayerData, QuestData, SaveData |
| Logic is reusable |
PathfindingHelper, DamageCalculator |
| System is complex |
Inventory, DialogueManager |
| Need multiple variants |
EnemyStats (goblin.tres, orc.tres) |
| Want to serialize data |
Save system, level data |
| Reduce Node overhead |
State machine, timers, counters |
When NOT to Create Custom Objects
| Use Built-in When... |
Use Node When... |
| Simple data (use Dictionary) |
Needs visual representation |
| One-time use (inline code) |
Needs scene tree access |
| Godot has it (Timer, etc.) |
Needs signals to UI |
Hints & Tips
- Start simple with built-in types
- Extract to custom objects when code gets complex
- Use
RefCounted for pure logic/data
- Use
Resource for reusable templates
- Use
Node only when scene tree is needed