SQLite en Godot: Guía Completa del Plugin godot-sqlite
Aprende a integrar y usar bases de datos SQLite en tus proyectos de Godot 4 para gestionar datos de manera eficiente y persistente.

¿Por qué SQLite en Videojuegos?
SQLite es una base de datos relacional ligera y embebida que ofrece una solución perfecta para el almacenamiento de datos en videojuegos. A diferencia de los archivos de configuración tradicionales como JSON o XML, SQLite proporciona:
- Consultas estructuradas: SQL para búsquedas complejas y eficientes
- Integridad de datos: Transacciones ACID y validación automática
- Performance superior: Optimizado para lecturas rápidas y escrituras eficientes
- Escalabilidad: Maneja desde pequeños saves hasta grandes inventarios
- Portabilidad: Un solo archivo contiene toda la base de datos
Instalación del Plugin godot-sqlite
El plugin godot-sqlite
es una extensión GDNative que permite usar SQLite directamente en Godot sin dependencias externas.
Método 1: Asset Library
La forma más sencilla es instalar desde el Asset Library de Godot:
- Abre tu proyecto en Godot 4
- Ve a AssetLib en la barra superior
- Busca "SQLite" o "godot-sqlite"
- Descarga e instala el plugin
- Habilita el plugin en Project Settings > Plugins
Método 2: Instalación Manual
Para mayor control, puedes instalar manualmente:
- Descarga el plugin desde GitHub
- Extrae los archivos en la carpeta
addons/
de tu proyecto - Estructura resultante:
addons/godot-sqlite/
- Habilita el plugin en configuración del proyecto
Configuración Inicial
Una vez instalado el plugin, necesitas configurar tu primera base de datos:
Creando la Conexión
extends Node
var db : SQLite
func _ready():
# Crear instancia de SQLite
db = SQLite.new()
# Abrir/crear base de datos
var db_path = "user://game_data.db"
db.path = db_path
db.open_db()
# Verificar conexión
if db.error_message != "":
print("Error al conectar: ", db.error_message)
else:
print("Conexión exitosa a SQLite")
# Crear tablas iniciales
create_tables()
Definiendo la Estructura de Datos
Es fundamental diseñar bien tu esquema de base de datos desde el inicio:
func create_tables():
# Tabla de jugadores
var query_players = """
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
level INTEGER DEFAULT 1,
experience INTEGER DEFAULT 0,
coins INTEGER DEFAULT 100,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME DEFAULT CURRENT_TIMESTAMP
)"""
# Tabla de inventario
var query_inventory = """
CREATE TABLE IF NOT EXISTS inventory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
item_id TEXT NOT NULL,
item_name TEXT NOT NULL,
quantity INTEGER DEFAULT 1,
rarity TEXT DEFAULT 'common',
obtained_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (player_id) REFERENCES players (id)
)"""
# Tabla de configuraciones
var query_settings = """
CREATE TABLE IF NOT EXISTS game_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)"""
# Ejecutar queries
db.query(query_players)
db.query(query_inventory)
db.query(query_settings)
Operaciones CRUD Básicas
Implementemos las operaciones fundamentales: Create, Read, Update, Delete.
CREATE - Insertar Datos
func create_player(username: String) -> int:
var query = "INSERT INTO players (username) VALUES (?)"
var params = [username]
db.query_with_bindings(query, params)
if db.error_message != "":
print("Error al crear jugador: ", db.error_message)
return -1
# Obtener ID del jugador creado
return db.last_insert_rowid
func add_item_to_inventory(player_id: int, item_id: String, item_name: String, quantity: int = 1, rarity: String = "common"):
var query = """
INSERT INTO inventory (player_id, item_id, item_name, quantity, rarity)
VALUES (?, ?, ?, ?, ?)
"""
var params = [player_id, item_id, item_name, quantity, rarity]
db.query_with_bindings(query, params)
if db.error_message != "":
print("Error al agregar item: ", db.error_message)
READ - Consultar Datos
func get_player_by_username(username: String) -> Dictionary:
var query = "SELECT * FROM players WHERE username = ?"
var params = [username]
db.query_with_bindings(query, params)
if db.query_result.size() > 0:
return db.query_result[0]
else:
return {}
func get_player_inventory(player_id: int) -> Array:
var query = """
SELECT item_id, item_name, quantity, rarity, obtained_at
FROM inventory
WHERE player_id = ?
ORDER BY obtained_at DESC
"""
var params = [player_id]
db.query_with_bindings(query, params)
return db.query_result
func get_top_players(limit: int = 10) -> Array:
var query = """
SELECT username, level, experience
FROM players
ORDER BY level DESC, experience DESC
LIMIT ?
"""
var params = [limit]
db.query_with_bindings(query, params)
return db.query_result
UPDATE - Actualizar Datos
func update_player_stats(player_id: int, level: int, experience: int, coins: int):
var query = """
UPDATE players
SET level = ?, experience = ?, coins = ?, last_login = CURRENT_TIMESTAMP
WHERE id = ?
"""
var params = [level, experience, coins, player_id]
db.query_with_bindings(query, params)
if db.error_message != "":
print("Error al actualizar stats: ", db.error_message)
func update_item_quantity(player_id: int, item_id: String, new_quantity: int):
if new_quantity <= 0:
# Si la cantidad es 0 o menor, eliminar el item
remove_item_from_inventory(player_id, item_id)
else:
var query = """
UPDATE inventory
SET quantity = ?
WHERE player_id = ? AND item_id = ?
"""
var params = [new_quantity, player_id, item_id]
db.query_with_bindings(query, params)
DELETE - Eliminar Datos
func remove_item_from_inventory(player_id: int, item_id: String):
var query = "DELETE FROM inventory WHERE player_id = ? AND item_id = ?"
var params = [player_id, item_id]
db.query_with_bindings(query, params)
if db.error_message != "":
print("Error al eliminar item: ", db.error_message)
func delete_player(player_id: int):
# Primero eliminar inventario (por integridad referencial)
var query_inventory = "DELETE FROM inventory WHERE player_id = ?"
db.query_with_bindings(query_inventory, [player_id])
# Luego eliminar jugador
var query_player = "DELETE FROM players WHERE id = ?"
db.query_with_bindings(query_player, [player_id])
Casos de Uso Avanzados
Sistema de Guardado Completo
Qué hace: Un sistema completo de save/load que maneja múltiples slots de guardado, almacena variables del juego en formato serializado y usa transacciones para garantizar integridad de datos.
Implementemos un sistema robusto para guardar y cargar el estado del juego:
class_name GameSaveManager
extends RefCounted
static var db: SQLite
static func initialize_save_system():
db = SQLite.new()
db.path = "user://savegame.db"
db.open_db()
create_save_tables()
static func create_save_tables():
# Tabla principal de partidas guardadas
var query_saves = """
CREATE TABLE IF NOT EXISTS game_saves (
slot_id INTEGER PRIMARY KEY,
save_name TEXT NOT NULL,
player_name TEXT NOT NULL,
level INTEGER NOT NULL,
playtime_seconds INTEGER DEFAULT 0,
current_scene TEXT NOT NULL,
save_date DATETIME DEFAULT CURRENT_TIMESTAMP,
screenshot_path TEXT
)"""
# Tabla de variables del juego
var query_variables = """
CREATE TABLE IF NOT EXISTS save_variables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slot_id INTEGER NOT NULL,
variable_name TEXT NOT NULL,
variable_value TEXT NOT NULL,
variable_type TEXT NOT NULL,
FOREIGN KEY (slot_id) REFERENCES game_saves (slot_id)
)"""
db.query(query_saves)
db.query(query_variables)
static func save_game(slot_id: int, save_data: Dictionary):
# Iniciar transacción para atomicidad
db.query("BEGIN TRANSACTION")
try:
# Eliminar guardado anterior si existe
delete_save_slot(slot_id, false)
# Guardar información principal
var main_query = """
INSERT INTO game_saves
(slot_id, save_name, player_name, level, playtime_seconds, current_scene)
VALUES (?, ?, ?, ?, ?, ?)
"""
var main_params = [
slot_id,
save_data.get("save_name", "Partida " + str(slot_id)),
save_data.get("player_name", "Jugador"),
save_data.get("level", 1),
save_data.get("playtime_seconds", 0),
save_data.get("current_scene", "")
]
db.query_with_bindings(main_query, main_params)
# Guardar variables del juego
for variable_name in save_data.get("variables", {}):
var value = save_data.variables[variable_name]
var type = typeof(value)
var var_query = """
INSERT INTO save_variables
(slot_id, variable_name, variable_value, variable_type)
VALUES (?, ?, ?, ?)
"""
var var_params = [slot_id, variable_name, str(value), get_type_name(type)]
db.query_with_bindings(var_query, var_params)
# Confirmar transacción
db.query("COMMIT")
print("Juego guardado exitosamente en slot ", slot_id)
except:
# Revertir en caso de error
db.query("ROLLBACK")
print("Error al guardar juego")
static func load_game(slot_id: int) -> Dictionary:
# Cargar información principal
var main_query = "SELECT * FROM game_saves WHERE slot_id = ?"
db.query_with_bindings(main_query, [slot_id])
if db.query_result.size() == 0:
return {}
var save_data = db.query_result[0]
# Cargar variables
var vars_query = """
SELECT variable_name, variable_value, variable_type
FROM save_variables
WHERE slot_id = ?
"""
db.query_with_bindings(vars_query, [slot_id])
var variables = {}
for row in db.query_result:
var value = parse_variable_value(row.variable_value, row.variable_type)
variables[row.variable_name] = value
save_data["variables"] = variables
return save_data
Sistema de Configuraciones Persistentes
Qué hace: Gestiona configuraciones del juego (volumen, controles, gráficos) de forma persistente, con categorización automática y soporte para diferentes tipos de datos.
class_name SettingsManager
extends RefCounted
static var db: SQLite
static func initialize():
db = SQLite.new()
db.path = "user://settings.db"
db.open_db()
create_settings_table()
load_default_settings()
static func create_settings_table():
var query = """
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
type TEXT NOT NULL,
category TEXT DEFAULT 'general',
description TEXT DEFAULT '',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)"""
db.query(query)
static func set_setting(key: String, value, category: String = "general", description: String = ""):
var query = """
INSERT OR REPLACE INTO settings
(key, value, type, category, description, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
"""
var params = [key, str(value), get_type_name(typeof(value)), category, description]
db.query_with_bindings(query, params)
static func get_setting(key: String, default_value = null):
var query = "SELECT value, type FROM settings WHERE key = ?"
db.query_with_bindings(query, [key])
if db.query_result.size() > 0:
var row = db.query_result[0]
return parse_variable_value(row.value, row.type)
return default_value
static func get_settings_by_category(category: String) -> Dictionary:
var query = "SELECT key, value, type FROM settings WHERE category = ?"
db.query_with_bindings(query, [category])
var settings = {}
for row in db.query_result:
settings[row.key] = parse_variable_value(row.value, row.type)
return settings
Optimización y Mejores Prácticas
Índices para Performance
Los índices mejoran significativamente la velocidad de consultas:
func create_indexes():
# Índice para búsquedas por username
db.query("CREATE INDEX IF NOT EXISTS idx_players_username ON players(username)")
# Índice para consultas de inventario por jugador
db.query("CREATE INDEX IF NOT EXISTS idx_inventory_player ON inventory(player_id)")
# Índice compuesto para búsquedas específicas
db.query("CREATE INDEX IF NOT EXISTS idx_inventory_player_item ON inventory(player_id, item_id)")
# Índice para ordenamiento por nivel
db.query("CREATE INDEX IF NOT EXISTS idx_players_level ON players(level DESC, experience DESC)")
Transacciones para Consistencia
Qué hace: Asegura que operaciones complejas (como transferencias de items entre jugadores) se ejecuten completamente o no se ejecuten en absoluto, manteniendo la integridad de los datos.
func transfer_items_between_players(from_player: int, to_player: int, item_id: String, quantity: int):
# Verificar que el jugador origen tenga suficientes items
var check_query = "SELECT quantity FROM inventory WHERE player_id = ? AND item_id = ?"
db.query_with_bindings(check_query, [from_player, item_id])
if db.query_result.size() == 0 or db.query_result[0].quantity < quantity:
print("No hay suficientes items para transferir")
return false
# Iniciar transacción
db.query("BEGIN TRANSACTION")
try:
# Reducir cantidad del jugador origen
var reduce_query = """
UPDATE inventory
SET quantity = quantity - ?
WHERE player_id = ? AND item_id = ?
"""
db.query_with_bindings(reduce_query, [quantity, from_player, item_id])
# Aumentar cantidad del jugador destino (o crear nuevo registro)
var increase_query = """
INSERT INTO inventory (player_id, item_id, item_name, quantity)
VALUES (?, ?, ?, ?)
ON CONFLICT(player_id, item_id) DO UPDATE SET quantity = quantity + ?
"""
db.query_with_bindings(increase_query, [to_player, item_id, "Item Name", quantity, quantity])
# Confirmar transacción
db.query("COMMIT")
return true
except:
# Revertir en caso de error
db.query("ROLLBACK")
return false
Pool de Conexiones (Avanzado)
Concepto: Un pool de conexiones mantiene múltiples conexiones de base de datos abiertas y las reutiliza según demanda, evitando el overhead de abrir/cerrar conexiones constantemente.
Qué hace: Gestiona un conjunto limitado de conexiones SQLite que pueden ser utilizadas por diferentes partes del juego de forma eficiente y thread-safe.
Para juegos más complejos, considera implementar un pool de conexiones:
class_name DatabasePool
extends RefCounted
static var connections: Array[SQLite] = []
static var available_connections: Array[SQLite] = []
static var max_connections: int = 5
static func initialize_pool():
for i in max_connections:
var db = SQLite.new()
db.path = "user://game_data.db"
db.open_db()
connections.append(db)
available_connections.append(db)
static func get_connection() -> SQLite:
if available_connections.size() > 0:
return available_connections.pop_back()
else:
# Esperar o crear conexión temporal
push_warning("Pool de conexiones agotado")
var temp_db = SQLite.new()
temp_db.path = "user://game_data.db"
temp_db.open_db()
return temp_db
static func return_connection(db: SQLite):
if db in connections:
available_connections.append(db)
Consideraciones de Seguridad
Validación de Entrada
Siempre valida y sanitiza los datos de entrada:
func safe_create_player(username: String) -> int:
# Validar longitud
if username.length() < 3 or username.length() > 20:
print("Username debe tener entre 3 y 20 caracteres")
return -1
# Validar caracteres permitidos
var regex = RegEx.new()
regex.compile("^[a-zA-Z0-9_]+$")
if not regex.search(username):
print("Username contiene caracteres no válidos")
return -1
# Verificar disponibilidad
if get_player_by_username(username).size() > 0:
print("Username ya existe")
return -1
return create_player(username)
Manejo de Errores Robusto
func robust_database_operation(operation: Callable) -> bool:
var max_retries = 3
var retry_count = 0
while retry_count < max_retries:
try:
operation.call()
if db.error_message == "":
return true
else:
print("Error en operación: ", db.error_message)
retry_count += 1
except error:
print("Excepción en base de datos: ", error)
retry_count += 1
# Esperar antes del siguiente intento
if retry_count < max_retries:
await get_tree().create_timer(0.1 * retry_count).timeout
print("Operación fallida después de ", max_retries, " intentos")
return false
Debugging y Herramientas
Logger de Consultas
class_name DatabaseLogger
extends RefCounted
static var log_queries: bool = false
static var log_file: FileAccess
static func enable_query_logging():
log_queries = true
log_file = FileAccess.open("user://database_queries.log", FileAccess.WRITE)
static func log_query(query: String, params: Array = []):
if not log_queries or not log_file:
return
var timestamp = Time.get_datetime_string_from_system()
var log_entry = "[%s] QUERY: %s" % [timestamp, query]
if params.size() > 0:
log_entry += " | PARAMS: " + str(params)
log_file.store_line(log_entry)
log_file.flush()
¡Eso es todo!
Has completado la guía completa de SQLite en Godot. Ahora tienes todas las herramientas para implementar sistemas de datos robustos en tus juegos, desde simples configuraciones hasta complejos sistemas de inventario y guardado.
Recuerda estos puntos clave:
- Planifica tu esquema: Diseña bien las tablas desde el inicio
- Usa transacciones: Para operaciones críticas que deben ser atómicas
- Implementa validación: Siempre valida datos de entrada
- Optimiza con índices: Para consultas frecuentes y complejas
- Maneja errores: Implementa recuperación robusta
- Documenta tus queries: Tu yo del futuro te lo agradecerá
¡Esperamos que esta guía te sea útil en tus proyectos! Si implementas algo genial con SQLite en Godot, nos encantaría escuchar sobre ello. ¡Hasta pronto! 🎮