- Presenta un enfoque para diseñar las mecánicas centrales de un juego con relaciones lógicas y predicados, en lugar de funciones, clases y variables de estado
- La razón por la que Prolog se siente más extraño que LISP no es tanto la sintaxis, sino que trata las estructuras de datos y los algoritmos alrededor de relaciones matemáticas
- Si vemos las filas de una base de datos relacional, las entradas y salidas de un circuito digital y los objetos de un juego como relaciones n-arias, el modelo declarativo de Prolog se conecta con el diseño impulsado por datos
- Aunque es difícil implementar en Prolog gráficos, audio, física e incluso I/O simple, sí puede aplicarse a las mecánicas de gameplay
- La parte 1 expresa las relaciones entre etiquetas y actores como predicados de Prolog a partir de world y actor, y deja los cambios en el tiempo y el game loop para la siguiente entrega
La problemática de aplicar Prolog a mecánicas de juego
- LISP y Prolog pertenecen a paradigmas de programación distintos de los lenguajes orientados a objetos comunes como C# y Java
- LISP es fuerte en programación funcional y patrones metalingüísticos, mientras que Prolog se trata como un lenguaje representativo de la programación lógica
- Gracias a conceptos como expresiones lambda y funciones de orden superior en C#, además de una base en ingeniería eléctrica y procesamiento de señales, LISP resultó más accesible de lo esperado
- En cambio, aunque la sintaxis de Prolog parece simple, su forma de tratar estructuras de datos y algoritmos como relaciones matemáticas fue una gran dificultad
- El enfoque de Prolog está conectado con varios modelos de ingeniería
- Cada fila de una base de datos relacional puede verse, en la sintaxis de Prolog, como un predicado n-ario
- El comportamiento de entrada y salida de los componentes de un circuito digital puede implementarse como una relación n-aria que combina puertos de entrada y salida
- Objetos como piezas de hardware o datos puros en memoria también pueden definirse como relaciones en programación lógica
- Así como en programación funcional se puede ver un objeto como una función, en programación lógica se puede ver un objeto como una relación
Mecánicas centrales de juego y diseño declarativo
- La programación lógica puede usarse para diseñar e implementar sistemas complejos, como las mecánicas centrales de gameplay de un videojuego
- Implementar todo un juego en Prolog es un gran desafío para un desarrollador de juegos común, salvo que sea con fines experimentales
- No todos los componentes de un juego encajan bien con Prolog
- Módulos simples de I/O
- Módulos gráficos
- Módulos de audio
- Módulos de física
- El objetivo de aplicación está más cerca de las mecánicas centrales que del juego completo
- Si un sistema de gameplay se compone como un conjunto de sentencias declarativas, puede ser más robusto y modular que un sistema imperativo, y estar más libre de edge cases confusos como las race conditions
- En esta metodología, el sistema debe diseñarse solo con relaciones lógicas y predicados
- Sin funciones
- Sin struct
- Sin class
- Sin interface
- Sin variables de estado
Modelo de World y Actor
- La clave de la programación de juegos basada en Prolog está en usar relaciones como el componente más primitivo, del mismo modo que resistencias, transistores, capacitores e inductores en un circuito eléctrico
- Un juego se divide, a grandes rasgos, en world y actor
- world es la escena donde ocurre todo
- actor es un objeto que pertenece al world
- actor se refiere a una entidad individual con nombre y atributos, como player, enemy, obstacle o item
- Los actores pueden interactuar entre sí o consigo mismos, y en ese proceso ocurren diversos eventos
- El gameplay puede verse como una cadena de estos eventos
- En este modelo, el world contiene varios actores, y cada actor tiene su propio estado y comportamiento
Expresar etiquetas como predicados unarios
- Cada actor se identifica con un nombre único
- En el ejemplo,
actor1 y actor2 representan al primer y segundo actor
- Una etiqueta es una palabra clave que se adjunta a un actor e indica qué es ese actor
bread(actor1). y bread(actor2). significan que tanto actor1 como actor2 son panes
bread(actor1).
bread(actor2).
- En Prolog,
bread es una relación unaria, y bread(actor1) y bread(actor2) son instancias individuales de esa relación
- Un actor puede tener varias etiquetas
- Si todos los panes deben ser
flammable y decomposable, adjuntar manualmente etiquetas a cada actor es tedioso y propenso a errores
- Con una Horn clause, cuando
bread(X) es verdadero, flammable(X) y decomposable(X) pasan a cumplirse automáticamente
flammable(X) :- bread(X).
decomposable(X) :- bread(X).
- Estas Horn clauses funcionan como datos de configuración del juego
- Son similares a las definiciones de características de character type, skill type o mission type en un documento de diseño técnico o una hoja de cálculo
- Es una estructura donde el type-specifier
bread incluye los atributos flammable y decomposable
- En una analogía con Unity, es parecido a poner los componentes
Flammable y Decomposable en un prefab Bread
- En un entorno orientado a objetos, es similar a que la clase
Bread implemente las interfaces IFlammable e IDecomposable
- En este contexto, las Horn clauses de Prolog cumplen el rol de definiciones de tipos de datos
Etiquetas personalizadas adjuntas a actores individuales
- Independientemente de las etiquetas predefinidas, se pueden adjuntar etiquetas personalizadas solo a un actor específico
- En el ejemplo, como un mago lanzó un hechizo sobre
actor2, el segundo pan, se necesita la etiqueta enchanted
enchanted(actor2).
flammable y decomposable son características que pertenecen a todos los panes
enchanted es una característica que solo se adjunta al pan especial encantado
Expresar relaciones entre actores como predicados binarios
- Un sistema de gameplay debe expresar no solo las características de actores individuales, sino también las relaciones entre actores
- Algunos ejemplos de relaciones son los siguientes
- En un ecosistema, el predator persigue a la prey, y la prey huye del predator
- En un dating simulator, el hombre intenta flirt con la mujer, y la mujer lo rechaza
- En un social simulator como The Sims, las personas son amigas, enemigas o algo intermedio
- En ajedrez, el bishop captura al rook en diagonal, y el rook captura al bishop en dirección ortogonal
- En Prolog, así como un predicado unario representa una característica de un actor, un predicado binario puede representar una relación entre dos actores
- Al usar Horn clauses, se pueden inferir relaciones dinámicamente a partir del conjunto de condiciones necesarias
Ejemplos de canEat y canSpoil
- Si
actor3 es humano y los humanos pueden comer pan, se puede definir la relación “si X es human e Y es bread, X puede comer Y”
human(actor3).
canEat(X, Y) :- human(X), bread(Y).
- Aquí,
canEat(X, Y) es una relación que se cumple entre X e Y
- Si
actor4 es fungus y el pan es decomposable, un fungus puede spoil a un actor decomposable
fungus(actor4).
canSpoil(X, Y) :- fungus(X), decomposable(Y).
canSpoil(X, Y) es la relación “si X es fungus e Y es decomposable, X puede spoil Y”
decomposable(Y) puede inferirse a partir de bread(Y) mediante la Horn clause anterior
Problema pendiente: cambios en el tiempo y game loop
- Los ejemplos hasta ahora expresan características y relaciones de actores, pero todos son estáticos
- Un juego no es un paisaje fijo: con el paso del tiempo, los actores deben moverse e interactuar
- Solo con el carácter declarativo de Prolog, parece difícil resolver fácilmente el problema de que las características cambien con el tiempo
- Cómo conceptualizar el game loop en Prolog continúa en Part 2
1 comentarios
Opiniones de Hacker News
Normalmente veo con malos ojos los artículos que solo tienen una Parte 1 y se quedan ahí, pero este de hecho sigue publicando entregas posteriores
https://thingspool.net/morsels/page-11.html (parte 2)
https://thingspool.net/morsels/page-12.html (parte 3)
https://thingspool.net/morsels/page-13.html (parte 4)
https://thingspool.net/morsels/page-14.html (parte 5)
Y continúa
Parece que estas técnicas podrían llevarse a la programación de juegos en Rust
En el diseño de juego de Breath of the Wild existe el concepto de motor químico. Normalmente, un motor de juego tiene un motor de física que calcula cómo interactúan los objetos en términos de movimiento; el motor químico trata cómo interactúan los materiales en un sentido alquímico.
Calcula las interacciones entre distintas sustancias, como un motor basado en reglas, y de ahí surgen interacciones sorprendentes, como que a una flecha se le pueda prender fuego porque está hecha de madera.
Enlace de YouTube: https://www.youtube.com/watch?v=QyMsF31NdNc&t=2354s
Los elementos basados en reglas de los juegos normalmente son relativamente simples y no necesitan ser de propósito general, así que parece que se codifican a mano. Cuando le preguntaron al creador de Baba is You si había implementado un motor Datalog, también dijo que no; supongo que Breath of the Wild será similar.
Aun así, suelo pensar que lo mejor para implementar un motor químico así, con experimentación rápida, sería usar un lenguaje lógico como Prolog o Datalog. SQL también se empezó a usar para mantener la flexibilidad de las consultas y al final se terminó desplegando tal cual; en el pasado seguramente había muchas quejas de que las consultas SQL eran lentas, pero esa flexibilidad era lo bastante útil como para invertir enormes cantidades de tiempo en hacerlas rápidas. Hoy casi nadie piensa: “puedo escribir a mano código imperativo más rápido que esta consulta”.
Entre los juegos que he probado hasta ahora, me parece bastante interesante.
https://store.steampowered.com/app/881100/Noita/
https://www.youtube.com/watch?v=prXuyMCgbTc
Probablemente los MUD también sean parecidos.
Muchas de las cosas que los juegos gráficos intentan hacer de forma innovadora ya las habían hecho los juegos de texto hace 40 años, en especial la ficción interactiva. Esto se notó más no tanto en las primeras obras de Infocom, sino en el renacimiento de excelentes juegos amateur de los 90 como Curses, Jigsaw, Anchorhead, Devours, Spider and Web; también en roguelikes como NetHack/Slash'EM, en CDDA: Bright Nights y en MUD en línea que trataban materiales e interacciones extrañas no solo en fantasía, sino también en ambientaciones cyberpunk o de ciencia ficción.
Incluso los mejores creadores de juegos, como Warren Spector, seguramente jugaron software libre, juegos indie y juegos excéntricos, y de ahí tomaron ideas nuevas de gameplay.
Creo que la referencia a NetHack en el Deus Ex original no era un simple easter egg, sino probablemente un homenaje al gameplay emergente inspirado en NetHack.
La virtud de Deus Ex no estaba en los gráficos ni en el universo, sino en lo que se podía hacer dentro del juego, y en que rompía la estructura lineal de los FPS a la vez que lo hacía más accesible que System Shock 1 y 2. En System Shock hubo aspectos fallidos en el ajuste de dificultad y en el acabado del gameplay. Arx Fatalis —hoy Libertatis— y Ultima Underworld son casos similares.
De reglas simples surge muchísima complejidad realmente interesante.
No es que solo existan los algoritmos de resolución implementados por lenguajes lógicos/relacionales de propósito general; además, cuanto más general sea algo, más ineficiente puede resultar para los problemas que un motor de juego necesita resolver eficientemente. A la inversa, si se extiende un lenguaje lógico con esas funciones de forma nativa, quizá haya que romper supuestos simplificadores o cambiar el orden de evaluación, lo que dificulta implementar el lenguaje de manera simple y eficiente.
Por ejemplo, quería combinar relaciones lógicas con restricciones lineales. Algo como
A <= (B-10.0) .OR. B <= (A-10.0), para restringir las posiciones centrales de dos objetos de juego de ancho 10.0 de modo que sus límites no se superpongan, sin importar cuál quede delante. Con sentencias así se podría componer cualitativamente una escena u objetos de juego complejos, y dejar que un solucionador de restricciones lineales elija las coordenadas exactas.El problema es que resolver una restricción lineal como
f(A) <= g(B)puede afectar a todas las variables relacionadas. Como A o B también pueden tener restricciones con C, D, E y F, si se ejecuta de inmediato al elegir el lado izquierdo o derecho de una fórmula lógica comoprop(X) .OR. prop(Y), se pueden invalidar otras proposiciones que ya habían quedado fijadas.Por eso, en lugar de interpretar el
<=de las variables lineales como una prueba que devuelve un resultado booleano, conviene interpretarlo como una instrucción que agregaf(A) <= g(B)a un almacén global de restricciones lineales, y más tarde ejecutar el solucionador sobre toda la matriz de variables lineales.Eso plantea preguntas de diseño: cómo sabe el lenguaje cuándo ya se terminaron de agregar restricciones y llegó el momento de resolverlas, o qué hacer con cláusulas lógicas donde realmente hay que probar valores y no imponer restricciones. Pero el punto central es que, en el contexto de un motor de juego, tanto en la resolución de restricciones lineales como en la programación lógica se necesita mucho más control sobre cuándo ejecutar cálculos largos y durante cuánto tiempo.
Es decir, aunque desde la perspectiva de un lenguaje lógico se diseñen bien las extensiones para que funcionen, desde la perspectiva del sistema externo sigue existiendo el problema de que el tiempo de búsqueda para resolver una parte del programa lógico puede dispararse de repente, y no siempre es fácil para el programador predecir cuándo ni dónde ocurrirá. En ese sentido, el código imperativo escrito a mano ni siquiera necesita ser más rápido; aunque sea más lento, basta con que tenga previsibilidad.
En la práctica, tampoco hace falta escribir todo directamente a mano. La clave es si el motor de reglas es un solucionador que el código imperativo del juego puede accionar y suspender desde afuera, o si todo el juego se ejecuta dentro del solucionador, es decir, dentro de un lenguaje relacional. Imaginar qué se podría hacer si todo el juego fuera relacional resulta atractivo, pero para usar esa técnica en todo el juego haría falta un solucionador relacional mágico que, en realidad, no existe.
Es interesante que sea una introducción a la programación de juegos con Prolog y vaya directo a juegos de acción. Cubre aspectos de tiempo real, líneas de tiempo, 3D, ECS y eventos.
Normalmente, los textos introductorios al desarrollo de juegos con Prolog empiezan con juegos de aventura, especialmente las aventuras de texto clásicas. Esto se debe a que los laberintos y los puzzles de inventario encajan directamente con los hechos y reglas de Prolog, y con los DSL.
Los juegos de cartas o de mesa también permiten expresar reglas muy cómodamente en Prolog, y se puede extender casi con facilidad un oponente genérico combinatorio básico, de forma similar a los planificadores en Prolog usados en robótica, logística, finanzas, industria, etc.
Cuando tomé una clase de IA, lo primero que aprendimos fue Prolog, y como tarea todos tuvimos que escribir un juego estilo adventure/colossal cave. Prolog encajaba bien con esa tarea, y me sorprendió la diversidad de juegos simples que se crearon en la clase.
Pienso que habría sido bueno haber recopilado todos los juegos que hicieron los demás estudiantes. Lo vimos solo unas semanas y luego pasamos a otros temas como CLIPS y Lisp.
En mi tarea hice Bureaucratic Maze [1], y eso también fue bastante intuitivo en Prolog.
[1]. http://logicmazes.com/bureau/index.htm
Pero en Inform6, gracias a la orientación a objetos aplicada al diseño del juego y a la forma de tratar directamente las relaciones entre los objetos del juego, como en ZMachine, la dificultad baja muchísimo y se vuelve casi trivial.
http://logicmazes.com/alice.html
alice.html:353 Uncaught TypeError: Cannot read properties of undefined (reading 'play')at playSound (alice.html:353:30)at finalize2 (alice.html:347:1)at :1:1Revisé por encima los 12 artículos y parecen buenos como introducción al uso de Prolog, pero todavía parece difícil decir que esto esté relacionado con la programación de juegos.
Puede que evolucione más adelante, pero hasta la lección 12 la mayoría trataba sobre modelar algunos conceptos de orientación a objetos en Prolog.
No sé si me estoy perdiendo algo, pero todavía no se aborda la interacción del usuario, que parecería ser una premisa de un juego. Había una parte que trataba brevemente el envío de mensajes, así que quizá esa sea la intención.
En una línea similar, leí con gusto un libro de programación de juegos con programación lógica que usa CLIPS en lugar de Prolog.
Adventures in Rule-Based Programming: A CLIPS Tutorial https://a.co/d/7wVOcZp
https://www.clipsrules.net/
Los libros de programación suelen ser pésimos en Kindle, pero quizá en una tableta Fire se vea aceptable.
Hay una superposición interesante entre ver el estado del juego como un conjunto de hechos y relaciones (Prolog) y la idea central de ECS de “esto es una base de datos”[1].
Personalmente estoy experimentando con Datascript como base de datos para el estado del juego, pero todavía es pronto para determinar si tendrá éxito. Me gusta ver que las ideas de este tutorial se mapean 1:1 con esa perspectiva.
Todavía no estoy seguro de si es posible definir reglas de forma más concisa que como lo hice ahora. En el paper de Stanford se pueden escribir reglas como
(<= (column ?n ?x) ...), pero en Datascript hay mucho más trabajo periférico moviendo datos.No sé si esto se debe a una limitación de Datascript/Datomic Datalog o a que mi comprensión es insuficiente.
Me interesa saber cómo estás abordando el experimento. Si tienes algo que puedas compartir o algún consejo sobre la forma en que lo estoy haciendo ahora, me interesaría mucho.
[1]: https://www.cs.uic.edu/~hinrichs/papers/love2006general.pdf
Es una forma fresca de tratar problemas de máquinas de estados en juegos con mucha lógica, por ejemplo simulaciones de ciudades. No se me había ocurrido usar Prolog de esta manera.
Parece que se podrían hacer cosas realmente geniales en debates sobre causalidad y relatividad. Por ejemplo, al generar un encuentro aleatorio con un NPC, se podría crear el historial de eventos de ese NPC incluyendo la cadena causal que el jugador inició en el pasado.
Como todo son relaciones, y existe el hecho de que el NPC existe ahora, junto con todos los estados del mundo y el historial de acciones observados, se puede rastrear hacia atrás sin simularlo de antemano y darle al NPC un pasado completamente coherente.
Nunca había pensado que el modelo común de actor-world/entity-trait encajara tan bien con el enfoque relacional de Prolog.
Pero también importan tiempos de ejecución predecibles y eficientes. Prolog suele tender a hacer búsqueda por fuerza bruta, unificando términos para satisfacer una consulta. Todavía no leí toda la serie, pero me pregunto si aborda este problema.
Mi primera suposición sería algo como escribir las restricciones conocidas sobre la solución en forma de una especie de “declaración de tipo”.