What is the difference between standard objects and immortal objects?

Objetos Inmortales: El Secreto de los Videojuegos

20/12/2005

Valoración: 4.19 (16094 votos)

Alguna vez te has preguntado, mientras exploras un vasto mundo abierto, ¿por qué ese PNJ crucial para la trama simplemente no puede morir, sin importar cuántas flechas le dispares? ¿O por qué la interfaz de usuario, con tu barra de vida y tu minimapa, siempre está ahí, inmutable y persistente? A menudo pensamos que son decisiones de diseño para proteger la narrativa del juego. Y si bien eso es cierto, detrás de esta invulnerabilidad se esconde un secreto técnico fascinante, una optimización de rendimiento tan profunda que es fundamental para que los juegos más grandes y complejos puedan funcionar sin problemas. Hoy vamos a sumergirnos en las entrañas del motor del juego para hablar de la diferencia entre los objetos estándar y los objetos inmortales.

How do I know if an object is immortal?
The trick (which maintains ABI compatibility) is pretty simple: a reference count value of 4294967295 now means an object is immortal, and the existing Py_INCREF and Py_DECREF macros have been updated to take that into account. DALL-E 3, GPT4All, PMTiles, sqlite-migrate, datasette-edit-schema - 30th October 2023

Este concepto, aunque extraído de la programación de bajo nivel como la que se discute en el desarrollo de CPython (la implementación de referencia del lenguaje Python), tiene un paralelismo perfecto en el desarrollo de videojuegos. Se trata de cómo un motor de juego gestiona eficientemente su memoria y sus recursos, decidiendo qué elementos viven y mueren, y cuáles están destinados a perdurar por toda la eternidad de nuestra partida.

Índice de Contenido

Objetos Mortales vs. Inmortales: El Ciclo de Vida Digital

Imagina que el motor de un juego es un gran director de escena. Cada elemento que ves en pantalla —un enemigo, un cofre del tesoro, una poción en el suelo— es un "objeto". El director necesita saber en todo momento cuántos actores (es decir, otras partes del juego) están interactuando con cada objeto. Para ello, utiliza un sistema llamado "conteo de referencias" o refcount.

Un objeto "mortal", como un goblin errante, nace (se genera en el mapa) con un contador de referencias. Cada vez que algo interactúa con él —por ejemplo, el jugador lo apunta, un script de IA lo activa, o se añade a una lista de enemigos en el área— su contador aumenta. Cuando esas interacciones terminan —el jugador mira hacia otro lado, la IA lo olvida— el contador disminuye. Cuando el contador de referencias de ese goblin llega a cero, el director de escena sabe que ya nadie lo necesita. En ese momento, el motor del juego lo elimina de la memoria para liberar recursos. ¡Puf! El goblin desaparece para siempre.

Ahora, pensemos en objetos como el propio personaje del jugador, la constante presencia de la interfaz de usuario, o valores fundamentales del juego como "Verdadero" o "Falso". Estos elementos son necesarios desde que inicias el juego hasta que lo cierras. Son, en esencia, inmortales. Sin embargo, hasta hace poco, muchos motores de juego trataban a estos objetos inmortales de la misma manera que a los mortales. Constantemente estaban aumentando y disminuyendo su contador de referencias, a pesar de que era una certeza absoluta que ese contador nunca llegaría a cero. Este es el núcleo del problema que los desarrolladores buscaron resolver.

El Costo Oculto de la Falsa Inmortalidad

Tratar a un objeto inmortal como si fuera mortal es ineficiente y tiene un impacto negativo real en el rendimiento, especialmente en proyectos que buscan una gran escalabilidad, como los MMOs o los juegos con mundos persistentes gigantescos. Aquí es donde la cosa se pone técnica, pero la analogía nos ayudará.

Cada vez que el contador de referencias de un objeto se modifica, incluso el de un objeto tan simple como `None` (la nada en programación) o `True` (verdadero), se produce una operación de escritura en la memoria. Esto obliga al procesador a invalidar su caché correspondiente, que es como una memoria a corto plazo ultrarrápida. Imagina que el procesador es un mago leyendo un grimorio. Cada vez que alguien le toca el hombro para decirle "oye, el rey sigue vivo", el mago pierde la concentración y tiene que volver a buscar la página en la que estaba. Ahora multiplica eso por millones de veces por segundo para objetos omnipresentes. El resultado es una pequeña pero constante pérdida de rendimiento.

Does object immortality have a public API?
Object immortality is meant to be an internal-only feature, so this proposal does not include any changes to public API or behavior (with one exception). As usual, we may still add some private (yet publicly accessible) API to do things like immortalize an object or tell if one is immortal.

El problema se agrava en los sistemas modernos con múltiples núcleos. Piensa en un procesador multinúcleo como un equipo de magos trabajando en paralelo. Si dos magos en torres distintas necesitan interactuar con el mismo objeto "inmortal" (por ejemplo, ambos necesitan confirmar que el cielo es azul), ambos intentarán actualizar su contador de referencias al mismo tiempo. Esto crea un conflicto, invalidando las cachés del otro y ralentizando todo el proceso. El "Global Interpreter Lock" (GIL) en Python actúa como un gran árbitro que solo deja hablar a un mago a la vez, lo que mitiga el problema pero no lo elimina y limita el verdadero paralelismo.

Finalmente, existe una técnica avanzada llamada "forking", donde un juego, tras alcanzar un estado inicial deseado (por ejemplo, el mundo cargado), se clona a sí mismo para crear múltiples procesos de trabajo. Esto es genial para el uso de la memoria. Sin embargo, si los objetos compartidos entre estos clones (como nuestro PNJ inmortal) siguen modificando su contador de referencias, el sistema operativo se ve obligado a crear copias separadas de esos objetos para cada clon, anulando gran parte del ahorro de memoria. Es como si cada clon del mundo necesitara su propia copia del rey porque todos insisten en tocarlo para ver si es real.

La Solución: Inmortalidad Real para un Rendimiento Superior

La solución propuesta e implementada es tan elegante como obvia: si sabemos que un objeto nunca va a morir, dejemos de contar sus referencias. El motor del juego ahora puede marcar ciertos objetos como verdaderamente inmortales. Esto se logra asignando a su contador de referencias un valor "mágico" específico, un número tan grande y particular que el sistema lo interpreta como una señal: "Este objeto es inmortal. No toques su contador. Nunca".

Cuando el motor del juego ve este valor mágico, las operaciones de aumentar o disminuir referencias simplemente no hacen nada. Son ignoradas. El mago ya no es interrumpido. Los beneficios son inmediatos:

  • Mejora del Rendimiento de la CPU: Se eliminan millones de escrituras innecesarias en memoria, lo que permite que las cachés de la CPU permanezcan válidas por más tiempo y todo funcione un poco más rápido.
  • Escalabilidad en Múltiples Núcleos: Múltiples hilos o procesos pueden acceder al mismo objeto inmortal sin conflictos de escritura, lo que simplifica enormemente el diseño de sistemas paralelos y permite un verdadero aprovechamiento del hardware moderno.
  • Optimización de la Memoria: En modelos de "forking", los objetos inmortales ahora pueden ser compartidos de verdad entre todos los procesos clonados sin generar copias, lo que resulta en un ahorro masivo de memoria.

Esta es una mejora fundamental que permite que un objeto sea verdaderamente inmutable. Sin modificaciones, no hay conflictos. Esto simplifica enormemente el camino hacia futuras optimizaciones, como tener un árbitro (GIL) por cada intérprete, lo que permitiría un paralelismo real a gran escala.

Compatibilidad y los Riesgos del Más Allá Digital

Introducir un cambio tan fundamental no está exento de riesgos, sobre todo en lo que respecta a la compatibilidad con código antiguo, como los mods. Los desarrolladores de motores tienen que garantizar que las extensiones más antiguas, compiladas con reglas diferentes, no rompan el juego.

El principal problema es que un mod antiguo podría no "entender" el concepto de inmortalidad. Podría usar funciones que modifican directamente el contador de referencias de un objeto. Al hacer esto sobre un objeto ahora inmortal, anularía todos los beneficios de rendimiento para ese objeto. Pero el peligro real viene de dos escenarios hipotéticos pero posibles:

Inmortalidad Accidental: Un objeto mortal normal, debido a un bug o a un bucle muy intenso en un mod, podría recibir tantas referencias que su contador alcance accidentalmente el valor mágico de la inmortalidad. Ese objeto nunca sería eliminado, convirtiéndose en una fuga de memoria permanente. Afortunadamente, en sistemas de 64 bits, este número es tan astronómicamente grande que tardarías miles de días en alcanzarlo incluso en un bucle que se ejecuta a 5 GHz. Es un riesgo teórico más que práctico.

What is the difference between standard objects and immortal objects?
A comparison of standard objects versus immortal objects. With standard objects, a user can guarantee that it will not mutate its type and/or its data. Immortality adds an extra guarantee that the runtime will not modify the reference count or the GC Header if present, enabling full object immutability.

Des-Inmortalización Accidental: Este es el caso más problemático. Un mod antiguo podría interactuar con un objeto inmortal (como `None`) y disminuir su contador de referencias repetidamente. Como el mod usa una versión antigua de la función, podría lograr bajar el contador por debajo del umbral mágico. Si esto sucede miles de millones de veces, el contador podría eventualmente llegar a cero. Si el mod luego intenta eliminar este objeto inmortal (que el resto del juego asume que siempre existirá), podría provocar un crash catastrófico. Los desarrolladores han implementado salvaguardas para esto, como hacer que el código de eliminación de estos objetos compruebe si deberían ser inmortales y, en lugar de eliminarlos, simplemente restablecer su contador al valor inmortal.

Tabla Comparativa: Objetos en el Juego

CaracterísticaObjeto Mortal (Enemigo Común)Objeto Inmortal (El Jugador)
Ciclo de VidaSe crea, se usa y se destruye.Existe durante toda la sesión de juego.
Gestión de RecursosSu contador de referencias se modifica constantemente. Se elimina cuando llega a cero.Su contador de referencias es un valor mágico y fijo. No se modifica.
Impacto en RendimientoCausa escrituras en memoria y posibles invalidaciones de caché. Es necesario para la limpieza.Elimina la sobrecarga de conteo, mejorando el rendimiento en CPU y sistemas multinúcleo.
Ejemplo en JuegosUn proyectil, un enemigo no jefe, un objeto consumible en el suelo.El personaje del jugador, la interfaz, un PNJ vital para la trama, el cielo.

Preguntas Frecuentes (FAQ)

¿Puedo hacer que cualquier objeto en un juego sea inmortal?

Técnicamente, cualquier objeto puede ser marcado como inmortal. Sin embargo, en la práctica, solo tiene sentido para aquellos que son verdaderamente persistentes y, preferiblemente, inmutables. Marcar un objeto mutable (que cambia), como un cofre de inventario compartido, como inmortal es posible, pero requiere garantías muy estrictas de que no será modificado de forma insegura por múltiples hilos a la vez, lo que anularía parte de los beneficios.

¿Cómo sabe un desarrollador si un objeto es inmortal?

La inmortalidad de los objetos es una característica interna del motor. No es algo que los diseñadores de niveles o los guionistas suelen controlar directamente a través de una API pública. Los programadores del motor deciden qué objetos fundamentales (como los tipos base, los singletons como `None`, etc.) deben ser inmortales desde el principio para garantizar la estabilidad y el rendimiento del núcleo del sistema.

¿Este cambio hará que todos mis juegos corran más rápido?

El impacto directo es más notable en aplicaciones a gran escala que hacen un uso intensivo de estos objetos inmortales, como servidores de juegos masivos o aplicaciones que utilizan técnicas de paralelismo complejas. En un juego indie más pequeño, el beneficio podría ser insignificante. Sin embargo, la mejora fundamental en la forma en que el motor maneja estos objetos sienta las bases para un código más limpio, eficiente y escalable para todos en el futuro.

En conclusión, la próxima vez que te encuentres con un personaje invencible o un elemento persistente en tu juego favorito, recuerda que su inmortalidad es más que una simple elección narrativa. Es el resultado de un diseño de ingeniería profundo que busca exprimir hasta la última gota de rendimiento del hardware, permitiendo la creación de los mundos virtuales cada vez más complejos y vastos que tanto amamos. Es una prueba de que, a veces, las optimizaciones más importantes son las que permanecen invisibles para el jugador.

Si quieres conocer otros artículos parecidos a Objetos Inmortales: El Secreto de los Videojuegos puedes visitar la categoría Juegos.

Subir