32blogby StudioMitsu

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.

10 min read
Contenido

¿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."

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:
    "The hero's HP is [player.hp]."

    $ player.hp -= 30

    "Hit! HP dropped to [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("Hero", hp=120, attack=15)

label start:
    scene bg room
    with fade

    "The adventure begins."
    "[player.name]'s HP: [player.hp]/[player.max_hp]"

    menu:
        "An enemy appears!"

        "Attack with sword":
            $ damage = player.take_damage(25)
            "Counterattack! Took [damage] damage!"
            jump check_status

        "Cast a spell":
            if player.use_mp(20):
                "Defeated the enemy with magic!"
                jump victory
            else:
                "Not enough 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():
        "You have fallen..."
        return

    "The adventure continues."
    return

label victory:
    "You won the battle!"
    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("Hero", hp=100)

label training:
    menu:
        "Which training do you choose?"

        "Strength training":
            $ player.attack += 3
            "Attack power rose to [player.attack]!"

        "Defense drills":
            $ player.defense += 2
            "Defense rose to [player.defense]!"

        "Meditation":
            $ player.max_mp += 10
            $ player.mp = player.max_mp
            "Max MP rose to [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.

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: