32blogby Studio Mitsu

Gestión de Estadísticas en Ren'Py: Patrones que No Rompen los Guardados

Implementa sistemas de estadísticas RPG y TRPG de forma segura en Ren'Py 8.5. Cubre define vs default, diseño de clases seguro para rollback y patrones de migración compatibles con guardados.

by omitsu12 min read
Contenido

La clave para sistemas de estadísticas seguros en Ren'Py: declara el estado mutable del juego con default (no define), define clases dentro de init python pero instáncialas con default, y hereda de renpy.store.object para soporte de rollback.

¿Construyendo un sistema de estadísticas en Ren'Py? Te encontrarás con las mismas paredes que todos. "Mis datos de guardado se rompieron." "El rollback no revierte mis valores." "Los jugadores con guardados antiguos crashean después de mi actualización." Estas tres quejas dominan r/RenPy y los Lemma Soft Forums.

Esta guía te muestra cómo construir sistemas de estadísticas en Ren'Py 8.5.2 que sobreviven guardados, rollback y actualizaciones del juego. Asume que has leído la guía de inicio y conoces lo básico de define y default.

Cinco formas de romper tus datos de guardado

Antes de escribir cualquier código, necesitas entender dónde se rompe el sistema de guardado de Ren'Py. Estos cinco patrones aparecen repetidamente en la comunidad.

Patrón 1: Declarar estadísticas con define

renpy
# Esto se romperá
define player_hp = 100

define declara una constante. No se incluye en los datos de guardado. Cuando el juego se reinicia, cualquier cambio de HP se pierde y el valor se restablece a 100.

Patrón 2: Crear instancias dentro de init python

renpy
init python:
    class PlayerStats:
        def __init__(self):
            self.hp = 100
            self.mp = 50

    # Esto se romperá
    player = PlayerStats()

Los bloques init python se re-ejecutan cada vez que el juego inicia. Crear una instancia aquí significa que se sobrescribe con valores iniciales en cada carga.

Patrón 3: Cambios de atributos de clase no rastreados por rollback

renpy
default player = PlayerStats()

label battle:
    $ player.hp -= 30
    # ← Hacer rollback aquí NO restaura hp a su valor anterior

El rollback de Ren'Py rastrea si la referencia de una variable cambió. player.hp -= 30 no cambia a qué apunta player (el objeto en sí), así que el rollback no lo detecta.

Patrón 4: Declarar listas y diccionarios con define

renpy
# Esto se romperá
define inventory = []

label start:
    $ inventory.append("potion")
    # Guardar → Cargar y el inventario vuelve a ser una lista vacía

La misma razón que el Patrón 1. Las listas, diccionarios y cualquier objeto mutable que cambie durante el juego necesitan default.

Patrón 5: Cambiar la estructura de la clase rompe guardados antiguos

renpy
init python:
    class PlayerStats:
        def __init__(self):
            self.hp = 100
            self.mp = 50
            # Agregado en v1.1
            self.stamina = 80

Los datos de guardado de la v1.0 no tienen un atributo stamina. Acceder a player.stamina después de cargar ese guardado crashea el juego.


Los patrones 1 y 4 se arreglan usando default. El Patrón 3 requiere cambiar la clase padre. Los Patrones 2 y 5 tienen cada uno soluciones dedicadas. Vamos a verlos.

define vs default: Cuándo usar cuál

La guía de inicio introdujo esta distinción. Aquí está la imagen más profunda.

renpy
# Constantes — no se guardan
define MAX_HP = 100
define e = Character("Ellen", who_color="#c8ffc8")

# Variables — se guardan
default player_hp = 100
default inventory = []
default game_flags = {}
DeclaraciónSe guardaRollbackUsar para
defineNoNoObjetos Character, valores de config, constantes de máximo
defaultHP, ítems, flags, estado del juego

La regla es simple. Si el valor cambia durante el juego, usa default. Si no, usa define.

La relación con init python

Las variables asignadas dentro de bloques init python tampoco se guardan.

renpy
init python:
    # Las definiciones de clase van aquí (OK)
    class PlayerStats:
        def __init__(self):
            self.hp = 100

    # NO instancies aquí (INCORRECTO)
    # player = PlayerStats()

Define clases en init python. Instancia con default. Esta es la regla.

renpy
# Definición de clase
init python:
    class PlayerStats:
        def __init__(self):
            self.hp = 100

# Instanciación (esto se guarda)
default player = PlayerStats()

Diseño de clases seguro para rollback

Incluso con default, los cambios de atributos de clase no son rastreados por rollback (Patrón 3). La solución es cambiar la clase padre.

Heredar de renpy.store.object

El object en el espacio de nombres store de Ren'Py es en realidad RevertableObject. Heredar de él hace que los cambios de atributos sean rastreables por rollback.

renpy
init python:
    class PlayerStats(renpy.store.object):
        def __init__(self):
            self.hp = 100
            self.mp = 50
            self.attack = 10
            self.defense = 5

default player = PlayerStats()

label start:
    "El HP del héroe es [player.hp]."

    $ player.hp -= 30

    "¡Golpe! El HP bajó a [player.hp]."

    # Hacer rollback aquí restaura correctamente player.hp a 100

Las clases que heredan de renpy.store.object tienen su __setattr__ enganchado, registrando todos los cambios de atributos. Esto permite que el rollback restaure los valores de atributos correctamente.

Las listas y diccionarios también son conscientes del rollback

De manera similar, las listas y diccionarios creados dentro de bloques init python se convierten automáticamente en versiones conscientes del rollback (RevertableList, RevertableDict). Lo mismo aplica para declaraciones default.

renpy
# Estos se convierten automáticamente en conscientes del rollback
default inventory = []
default game_flags = {}

Si necesitas explícitamente tipos built-in puros de Python (para datos que no deberían ser revertidos), usa python_list() o python_dict(). En la mayoría de los casos, las versiones conscientes del rollback por defecto son lo que quieres.

Construyendo un sistema de estadísticas

Es hora de juntar todo en un sistema de estadísticas funcional.

Definición de clase

renpy
init python:
    class PlayerStats(renpy.store.object):
        def __init__(self, name, hp=100, mp=50, attack=10, defense=5):
            self.name = name
            self.max_hp = hp
            self.hp = hp
            self.max_mp = mp
            self.mp = mp
            self.attack = attack
            self.defense = defense

        def take_damage(self, amount):
            actual = max(0, amount - self.defense)
            self.hp = max(0, self.hp - actual)
            return actual

        def heal(self, amount):
            self.hp = min(self.max_hp, self.hp + amount)

        def use_mp(self, cost):
            if self.mp < cost:
                return False
            self.mp -= cost
            return True

        def is_alive(self):
            return self.hp > 0

Los límites superior e inferior se aplican con max() / min(). Esto previene que HP sea negativo o exceda el máximo.

Instanciación y uso

renpy
default player = PlayerStats("Héroe", hp=120, attack=15)

label start:
    scene bg room
    with fade

    "La aventura comienza."
    "HP de [player.name]: [player.hp]/[player.max_hp]"

    menu:
        "¡Aparece un enemigo!"

        "Atacar con espada":
            $ damage = player.take_damage(25)
            "¡Contraataque! ¡Recibiste [damage] de daño!"
            jump check_status

        "Lanzar un hechizo":
            if player.use_mp(20):
                "¡Derrotaste al enemigo con magia!"
                jump victory
            else:
                "¡No tienes suficiente MP!"
                $ player.take_damage(30)
                jump check_status

label check_status:
    "[player.name] HP: [player.hp]/[player.max_hp]  MP: [player.mp]/[player.max_mp]"

    if not player.is_alive():
        "Has caído..."
        return

    "La aventura continúa."
    return

label victory:
    "¡Ganaste la batalla!"
    return

Screen de HUD de estadísticas

Para mostrar estadísticas en pantalla en todo momento, define una screen.

renpy
screen stats_hud():
    frame:
        xalign 1.0
        yalign 0.0
        xpadding 15
        ypadding 10

        vbox:
            spacing 5
            text "[player.name]" size 18
            text "HP: [player.hp]/[player.max_hp]" size 14
            text "MP: [player.mp]/[player.max_mp]" size 14
            text "ATK: [player.attack]  DEF: [player.defense]" size 14

label start:
    show screen stats_hud
    # Las estadísticas permanecen visibles en la esquina superior derecha desde aquí

Cambios de estadísticas desde opciones

El corazón de cualquier RPG o TRPG — opciones que moldean tu personaje.

renpy
default player = PlayerStats("Héroe", hp=100)

label training:
    menu:
        "¿Qué entrenamiento eliges?"

        "Entrenamiento de fuerza":
            $ player.attack += 3
            "¡El poder de ataque subió a [player.attack]!"

        "Ejercicios de defensa":
            $ player.defense += 2
            "¡La defensa subió a [player.defense]!"

        "Meditación":
            $ player.max_mp += 10
            $ player.mp = player.max_mp
            "¡El MP máximo subió a [player.max_mp]!"

    jump next_scene

Porque la clase hereda de renpy.store.object, todos los cambios de atributos están cubiertos por guardado y rollback.

Manteniendo guardados antiguos vivos después de actualizaciones

Después de lanzar tu juego, inevitablemente querrás agregar nuevos atributos de estadísticas. Pero los datos de guardado antiguos no tienen esos atributos, y acceder a ellos crashea el juego (Patrón 5).

default auto-llena variables simples

Las variables simples declaradas con default se inicializan automáticamente a su valor por defecto si no existen al momento de cargar.

renpy
# v1.0
default player_hp = 100

# Agregado en v1.1 — los guardados antiguos automáticamente obtienen 80
default player_stamina = 80

Esto solo funciona para variables de nivel superior declaradas con default. No aplica a atributos de clase.

Migra con label after_load

Para adiciones o cambios de atributos de clase, escribe lógica de migración en label after_load. Este label se ejecuta automáticamente cada vez que se cargan datos de guardado.

renpy
init python:
    class PlayerStats(renpy.store.object):
        def __init__(self):
            self.hp = 100
            self.mp = 50
            self.attack = 10
            self.defense = 5
            # Agregado en v1.1
            self.stamina = 80
            self.max_stamina = 80

default player = PlayerStats()

label after_load:
    # v1.1: Manejar guardados antiguos sin stamina
    if not hasattr(player, "stamina"):
        $ player.stamina = 80
        $ player.max_stamina = 80

    return

Usa hasattr() para verificar si el atributo existe antes de agregarlo. De esta forma, los datos de guardado de v1.0 se cargan correctamente con stamina correctamente inicializada.

Patrón de seguimiento de versión

Cuando las migraciones abarcan múltiples versiones, rastrear un número de versión mantiene el código organizado.

renpy
default save_version = 1

label after_load:
    if save_version < 2:
        # v1.0 → v1.1: Agregar stamina
        if not hasattr(player, "stamina"):
            $ player.stamina = 80
            $ player.max_stamina = 80
        $ save_version = 2

    if save_version < 3:
        # v1.1 → v1.2: Agregar luck
        if not hasattr(player, "luck"):
            $ player.luck = 5
        $ save_version = 3

    return

save_version se declara con default, así que el auto-llenado se activa. Los guardados antiguos no tendrán save_version, así que se inicializa a 1, y todas las migraciones se ejecutan en orden.

FAQ

¿Por qué mi variable se reinicia después de guardar y cargar?

Probablemente estás usando define en vez de default. define crea una constante que no se incluye en los datos de guardado. Cambia a default y el valor persistirá entre guardados. Consulta la referencia oficial de Python Statements.

¿Cuál es la diferencia entre define y default?

define declara constantes (no se guardan, no se revierten). default declara variables (se guardan y se revierten). Si el valor cambia durante el juego, usa default. Si no cambia, usa define.

¿Es necesario heredar explícitamente de renpy.store.object?

No estrictamente. Las clases definidas dentro de bloques init python usan automáticamente RevertableObject como clase base. Pero escribir la herencia explícitamente — class PlayerStats(renpy.store.object) — hace la intención clara y evita sorpresas si luego mueves la clase a un archivo .py.

¿Cómo evito que los guardados antiguos crasheen al agregar nuevas estadísticas?

Usa label after_load con verificaciones hasattr(). Este label se ejecuta cada vez que se carga un guardado, así que puedes agregar atributos faltantes a objetos existentes. Para migraciones complejas de múltiples versiones, usa una variable save_version.

¿Puedo renombrar una clase sin romper los guardados?

No. Ren'Py usa pickle de Python para la serialización, que almacena el nombre de la clase y la ruta del módulo. Renombrar una clase o moverla a un archivo diferente hace que pickle no pueda encontrarla, crasheando al cargar.

¿Cómo muestro estadísticas en pantalla durante el juego?

Define una screen con elementos de texto que referencien tus variables de estadísticas usando la interpolación de texto de Ren'Py ([player.hp]), luego usa show screen stats_hud en tu label. La screen se actualiza automáticamente cuando los valores cambian.

¿Las listas y diccionarios son automáticamente seguros para rollback?

Sí, cuando se crean dentro de bloques init python o mediante declaraciones default. Ren'Py los reemplaza con RevertableList y RevertableDict. Si necesitas tipos puros de Python (raro), usa python_list() o python_dict().

Conclusión

Esto es lo que cubrimos:

  • Cinco patrones que rompen guardados: Declaraciones define, instanciación en init python, atributos ciegos al rollback, define mutable y atributos faltantes después de actualizaciones
  • define vs default: Cualquier cosa que cambie durante el juego usa default; todo lo demás usa define
  • Clases seguras para rollback: Hereda de renpy.store.object e instancia con default
  • Implementación de sistema de estadísticas: Aplicación de límites, screen de HUD, cambios de estadísticas por opciones
  • Migración compatible con guardados: label after_load + hasattr() + seguimiento de versión

Si eres nuevo en Ren'Py, comienza con la guía de inicio para configuración y lo básico.

Recursos oficiales:

  • Python Statements — referencia oficial para define / default / init python
  • Save, Load, and Rollback — cómo funcionan el sistema de guardado y rollback
  • Store Variables — lista de variables en el espacio de nombres store
  • renpy/renpy — código fuente de Ren'Py en GitHub (implementación de rollback en renpy/revertable/)
  • Python pickle — el formato de serialización que Ren'Py usa para datos de guardado
  • r/RenPy — comunidad para preguntas y discusión

Artículos relacionados: