- Aborda uma proposta de projetar os mecanismos centrais de um jogo com relações lógicas e predicados, em vez de funções, classes e variáveis de estado
- O motivo de Prolog parecer mais estranho que LISP não era a sintaxe, mas o fato de lidar com estruturas de dados e algoritmos com foco em relações matemáticas
- Se enxergarmos linhas de bancos de dados relacionais, entradas e saídas de circuitos digitais e objetos de jogo como relações n-árias, o modelo declarativo de Prolog se conecta ao design orientado a dados
- Embora seja difícil implementar gráficos, áudio, física e I/O simples em Prolog, há potencial de aplicação nos mecanismos de gameplay
- A Parte 1 usa world e actor como base para expressar tags e relações entre actors com predicados de Prolog, deixando as mudanças no tempo e o loop do jogo para a próxima parte
A motivação para aplicar Prolog a mecanismos de jogo
- LISP e Prolog pertencem a paradigmas de programação diferentes de linguagens orientadas a objetos comuns como C# e Java
- LISP é forte em programação funcional e padrões metalinguísticos, enquanto Prolog é tratado como a linguagem representativa da programação lógica
- Foi possível abordar LISP com mais facilidade do que o esperado graças a conceitos como expressões lambda e funções de ordem superior em C#, além de uma base em engenharia elétrica e processamento de sinais
- Já Prolog, embora pareça ter uma sintaxe simples, apresentava como grande dificuldade a forma de tratar estruturas de dados e algoritmos como relações matemáticas
- A abordagem de Prolog toca em vários modelos de engenharia
- Cada linha de um banco de dados relacional pode ser vista como um predicado n-ário na sintaxe de Prolog
- O comportamento de entrada e saída de componentes de circuitos digitais pode ser implementado como uma relação n-ária que combina portas de entrada e saída
- Objetos como partes de hardware ou dados puros na memória também podem ser definidos como relações na programação lógica
- Assim como na programação funcional é possível enxergar objetos como funções, na programação lógica é possível enxergar objetos como relações
Mecanismos centrais de jogo e design declarativo
- A programação lógica pode ser usada para projetar e implementar sistemas complexos, como os mecanismos centrais de gameplay de videogames
- Implementar um jogo inteiro em Prolog é um grande desafio para um desenvolvedor de jogos comum, a menos que o objetivo seja experimentação
- Nem todos os componentes de um jogo se encaixam bem em Prolog
- módulos de I/O simples
- módulos gráficos
- módulos de áudio
- módulos de física
- O alvo de aplicação está mais próximo dos mecanismos centrais do que do jogo inteiro
- Se o sistema de gameplay for composto como um conjunto de sentenças declarativas, ele pode ser mais robusto e modular do que um sistema imperativo, além de ficar mais livre de edge cases confusos como race conditions
- Nessa metodologia, o sistema deve ser projetado apenas com relações lógicas e predicados
- sem funções
- sem
struct
- sem classes
- sem interfaces
- sem variáveis de estado
Modelo de World e Actor
- O núcleo da programação de jogos baseada em Prolog está em usar relações como bloco de construção mais primitivo, como resistores, transistores, capacitores e indutores em circuitos elétricos
- O jogo se divide, em linhas gerais, em world e actor
- world é a cena onde tudo acontece
- actor é o objeto que pertence ao world
- Actor se refere a entidades individuais com nome e atributos, como player, enemy, obstacle e item
- Actors podem interagir entre si ou consigo mesmos, e nesse processo vários eventos acontecem
- O gameplay pode ser visto como uma cadeia desses eventos
- Nesse modelo, o world contém vários actors, e cada actor tem seu próprio estado e comportamento
Expressando tags como predicados unários
- Cada actor é identificado por um nome único
- No exemplo,
actor1 e actor2 representam o primeiro e o segundo actor
- Tags são palavras-chave associadas ao actor e indicam o que ele é
bread(actor1). e bread(actor2). significam que actor1 e actor2 são ambos pães
bread(actor1).
bread(actor2).
- Em Prolog,
bread é uma relação unária, e bread(actor1) e bread(actor2) são instâncias individuais dessa relação
- Um actor pode ter várias tags
- Se todo pão deve ser
flammable e decomposable, anexar manualmente essas tags a cada actor é trabalhoso e propenso a erros
- Com cláusulas de Horn, quando
bread(X) for verdadeiro, flammable(X) e decomposable(X) passam a valer automaticamente
flammable(X) :- bread(X).
decomposable(X) :- bread(X).
- Essas cláusulas de Horn funcionam como dados de configuração do jogo
- São semelhantes à definição de características de character type, skill type e mission type em documentos de design técnico ou planilhas
- É uma estrutura em que o especificador de tipo
bread inclui as propriedades flammable e decomposable
- Em uma analogia com Unity, é parecido com colocar componentes
Flammable e Decomposable em um prefab Bread
- Em um ambiente orientado a objetos, é semelhante a uma classe
Bread implementar as interfaces IFlammable e IDecomposable
- Nesse contexto, as cláusulas de Horn de Prolog cumprem o papel de definição de tipo de dado
Tags customizadas aplicadas a actors individuais
- Separadamente das tags predefinidas, é possível anexar tags customizadas apenas a actors específicos
- No exemplo, um mago lançou um encantamento sobre o segundo pão,
actor2, então a tag enchanted é necessária
enchanted(actor2).
flammable e decomposable são características de todos os pães
enchanted é uma característica aplicada apenas ao pão especial que foi encantado
Expressando relações entre actors como predicados binários
- Um sistema de gameplay precisa expressar não apenas as características de actors individuais, mas também as relações entre actors
- Exemplos de relações incluem:
- em um ecossistema, o predator persegue o prey, e o prey foge do predator
- em um dating simulator, o homem tenta flertar com a mulher e a mulher o rejeita
- em um simulador social como The Sims, as pessoas podem ser amigas, inimigas ou algo entre os dois
- no xadrez, o bishop captura o rook na diagonal, e o rook captura o bishop em linhas ortogonais
- Em Prolog, assim como predicados unários representam características de um único actor, predicados binários podem representar relações entre dois actors
- Com cláusulas de Horn, é possível inferir dinamicamente as relações a partir do conjunto de condições necessário
Exemplos de canEat e canSpoil
- Se
actor3 é um humano e humanos podem comer pão, é possível definir a relação “se X é human e Y é bread, então X pode comer Y”
human(actor3).
canEat(X, Y) :- human(X), bread(Y).
- Aqui,
canEat(X, Y) é uma relação válida entre X e Y
- Se
actor4 é um fungus e pão é decomposable, então um fungus pode estragar actors decomposable
fungus(actor4).
canSpoil(X, Y) :- fungus(X), decomposable(Y).
canSpoil(X, Y) é a relação “se X é fungus e Y é decomposable, então X pode estragar Y”
decomposable(Y) pode ser inferido anteriormente a partir de bread(Y) por meio de uma cláusula de Horn
Problema ainda em aberto: mudanças ao longo do tempo e loop do jogo
- Até aqui, os exemplos expressam características e relações entre actors, mas todos são estáticos
- Um jogo não é uma paisagem fixa; com o passar do tempo, os actors precisam se mover e interagir
- Só a natureza declarativa de Prolog não parece resolver facilmente o problema de características que mudam com o tempo
- A discussão sobre como conceituar o loop do jogo em Prolog continua na Parte 2
1 comentários
Comentários do Hacker News
Normalmente eu tendo a ver com maus olhos textos que têm só a Parte 1 e param por aí, mas neste caso as continuações de fato seguem saindo
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)
E continua
Acho que dá para levar essas técnicas para programação de jogos em Rust
No design de jogo de Breath of the Wild existe o conceito de motor químico. Normalmente, engines de jogo têm um motor de física que calcula como os objetos interagem em termos de movimento; já o motor químico trata de como os materiais interagem em um sentido alquímico
Como um motor baseado em regras, ele calcula as interações entre substâncias diferentes, gerando interações surpreendentes — por exemplo, uma flecha poder pegar fogo porque é feita de madeira
Link do YouTube: https://www.youtube.com/watch?v=QyMsF31NdNc&t=2354s
Os elementos baseados em regras dos jogos em geral são relativamente simples e não precisam ser genéricos, então parecem ser codificados à mão. Quando perguntei ao criador de Baba is You se ele tinha implementado um motor Datalog, ele disse que não; imagino que Breath of the Wild seja parecido
Ainda assim, muitas vezes penso que o melhor para esse tipo de motor químico seria implementá-lo em uma linguagem lógica como Prolog ou Datalog para permitir experimentação rápida. SQL também começou sendo usado para manter a flexibilidade das consultas e acabou sendo distribuído assim mesmo; no passado provavelmente havia muitas reclamações de que consultas SQL eram lentas, mas essa flexibilidade era útil o bastante para que se investissem inúmeras horas em torná-las rápidas. Hoje quase ninguém pensa: “consigo escrever código imperativo manualmente mais rápido do que esta consulta”
Entre os jogos que já joguei, é um dos mais interessantes
https://store.steampowered.com/app/881100/Noita/
https://www.youtube.com/watch?v=prXuyMCgbTc
MUDs provavelmente são parecidos
As coisas que jogos gráficos tentam fazer de forma inovadora já eram feitas 40 anos atrás por jogos de texto, especialmente ficção interativa. Isso ficou ainda mais evidente não tanto nos primeiros títulos da Infocom, mas no renascimento de excelentes jogos amadores dos anos 90, como Curses, Jigsaw, Anchorhead, Devours, Spider and Web; em roguelikes como NetHack/Slash'EM e CDDA: Bright Nights; e também em MUDs online que lidavam com materiais e interações estranhas em ambientações não só de fantasia, mas também cyberpunk ou ficção científica
É provável que grandes criadores de jogos, como Warren Spector, tenham jogado jogos de software livre, jogos indies e jogos excêntricos, trazendo dali novas ideias de gameplay
Vejo a referência a NetHack no Deus Ex original não como um simples easter egg, mas provavelmente como uma homenagem ao gameplay emergente inspirado por NetHack
O ponto forte de Deus Ex não era o gráfico nem o universo, mas o que era possível fazer dentro do jogo; ele quebrou a estrutura linear dos FPS ao mesmo tempo em que se tornou mais acessível do que System Shock 1 e 2. System Shock falhou em alguns aspectos de ajuste de dificuldade e acabamento do gameplay. Arx Fatalis, hoje Libertatis, e Ultima Underworld são casos parecidos
Regras simples geram muita complexidade realmente interessante
Não é que só sejam possíveis os algoritmos de solução implementados por linguagens lógicas/relacionais genéricas; quanto mais genéricas elas são, mais ineficientes podem se tornar para os problemas que uma engine de jogo precisa resolver com eficiência. Por outro lado, se você estende uma linguagem lógica nativamente com esses recursos, pode ser necessário quebrar pressupostos simplificadores ou alterar a ordem de avaliação, o que dificulta implementar a linguagem de forma simples e eficiente
Por exemplo, eu queria usar relações lógicas junto com restrições lineares. Algo como
A <= (B-10.0) .OR. B <= (A-10.0), restringindo a posição central de dois objetos de jogo com largura 10.0 para que suas bordas não se sobreponham, sem me importar qual deles vem antes. Com esse tipo de instrução, seria possível compor cenas ou objetos de jogo complexos de maneira qualitativa, deixando um solucionador de restrições lineares escolher as coordenadas exatasO problema é que resolver uma restrição linear como
f(A) <= g(B)pode afetar todas as variáveis relacionadas. Como A ou B também podem ter restrições com C, D, E e F, se você executar imediatamente ao escolher o lado esquerdo ou direito da expressão lógicaprop(X) .OR. prop(Y), outras proposições já fixadas podem ser invalidadasPor isso, em vez de interpretar o
<=de variáveis lineares como um teste que produz um resultado booleano, é melhor interpretá-lo como um comando que adicionaf(A) <= g(B)a um repositório global de restrições lineares e depois executar o solucionador sobre toda a matriz de variáveis linearesAí surgem perguntas de design: como a linguagem sabe quando terminou de adicionar restrições e chegou a hora de resolvê-las? O que fazer com cláusulas lógicas em que é preciso de fato testar um valor, e não criar uma restrição? Mas o ponto central é que, no contexto de engines de jogos, tanto na solução de restrições lineares quanto na programação lógica, é preciso ter mais controle sobre quando cálculos longos são executados e por quanto tempo
Ou seja, mesmo que, do ponto de vista de uma linguagem lógica, você projete bem os recursos de extensão para que funcionem, do ponto de vista do sistema externo ainda resta o problema de que o tempo de busca para resolver uma parte do programa lógico pode explodir de repente, e nem sempre é fácil para o programador prever quando e onde isso acontecerá. Nesse sentido, o código imperativo escrito à mão nem precisa ser mais rápido; basta que tenha previsibilidade, mesmo que seja mais lento
Na prática, também não é necessário escrever tudo manualmente. O ponto principal é se o motor de regras é um solucionador que o código imperativo do jogo aciona e pode interromper de fora, ou se o jogo inteiro roda dentro do solucionador, isto é, dentro de uma linguagem relacional. Imaginar o que seria possível se o jogo inteiro fosse relacional é fascinante, mas, para usar essa técnica no jogo inteiro, seria necessário um solucionador relacional meio mágico que não existe de verdade
É interessante que seja uma introdução à programação de jogos com Prolog, mas vá direto para jogos de ação. Ela aborda aspectos de tempo real, linha do tempo, 3D, ECS e eventos.
Normalmente, guias introdutórios de desenvolvimento de jogos em Prolog começam com jogos de aventura, especialmente aventuras textuais clássicas. Isso porque labirintos e quebra-cabeças de inventário se encaixam diretamente nos fatos e regras do Prolog e em DSLs.
Jogos de cartas ou de tabuleiro também permitem expressar regras de forma muito conveniente em Prolog, e é possível estender quase facilmente oponentes genéricos básicos de jogos combinatórios, de modo semelhante aos planejadores em Prolog usados em robótica, logística, finanças, indústria etc.
Quando fiz uma disciplina de IA, a primeira coisa que aprendemos foi Prolog, e todos tivemos de escrever, como tarefa, um jogo no estilo adventure/colossal cave. Prolog se encaixava bem nessa tarefa, e a variedade dos jogos simples criados na turma foi surpreendente.
Penso que teria sido bom se tivéssemos guardado todos os jogos feitos pelos outros alunos. Depois de apenas algumas semanas, passamos para outros temas, como CLIPS e Lisp.
Na minha tarefa, criei o Bureaucratic Maze [1], e isso também foi bastante intuitivo em Prolog.
[1]. http://logicmazes.com/bureau/index.htm
Mas em Inform6, graças à orientação a objetos aplicada ao design do jogo e à forma de lidar diretamente com as relações entre objetos dentro do jogo, como na ZMachine, a dificuldade cai muito e fica quase 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:1Dei uma passada pelos 12 textos e, embora pareçam bons como introdução ao uso de Prolog, ainda parece forçado dizer que isso tenha relação com programação de jogos.
Pode evoluir mais adiante, mas até a 12ª aula o conteúdo era, em grande parte, sobre tentar modelar alguns conceitos de orientação a objetos em Prolog.
Não sei se deixei passar algo, mas a interação com o usuário, que parece ser uma premissa de um jogo, ainda não foi abordada. Houve uma parte curta sobre envio de mensagens, então talvez fosse isso que tinham em mente.
Em uma linha parecida, li com interesse um livro de programação de jogos em programação lógica que usa CLIPS em vez de Prolog.
Adventures in Rule-Based Programming: A CLIPS Tutorial https://a.co/d/7wVOcZp
https://www.clipsrules.net/
Livros de programação costumam ficar péssimos no Kindle, mas talvez sejam aceitáveis vistos em um tablet Fire.
Há uma sobreposição interessante entre a visão do estado do jogo como um conjunto de fatos e relações (Prolog) e a ideia central de ECS de que “isto é um banco de dados”[1].
Pessoalmente, estou experimentando Datascript como banco de dados de estado de jogo, mas ainda é cedo para julgar se dará certo. É bom ver as ideias deste tutorial mapearem 1:1 para essa perspectiva.
Ainda não tenho certeza se é impossível definir as regras de forma mais concisa do que fiz agora. Nos artigos de Stanford, é possível escrever regras como
(<= (column ?n ?x) ...), mas em Datascript há muito mais trabalho periférico para mover dados.Não sei se isso se deve a uma limitação do Datalog do Datascript/Datomic ou à minha falta de entendimento.
Fico curioso sobre como você está abordando seus experimentos. Se tiver algo para compartilhar ou algum conselho sobre a forma como estou fazendo agora, eu teria muito interesse.
[1]: https://www.cs.uic.edu/~hinrichs/papers/love2006general.pdf
É uma abordagem nova para lidar com problemas de máquinas de estados em jogos com muita lógica, como simulações urbanas. Nunca tinha pensado em usar Prolog dessa forma.
Parece que dá para fazer coisas muito legais em discussões sobre causalidade e relatividade. Por exemplo, ao gerar um encontro aleatório com um NPC, seria possível criar um histórico de eventos desse NPC que inclua a cadeia causal iniciada pelo jogador no passado.
Como tudo é relação, e há o fato de o NPC existir agora, além de todos os estados do mundo observados e do histórico de ações, seria possível rastrear para trás e dar ao NPC um passado totalmente consistente, sem precisar simulá-lo previamente.
Eu nunca tinha pensado que o modelo comum de actor-world/entity-trait se encaixava tão bem com a abordagem relacional do Prolog.
Ainda assim, tempos de execução previsíveis e eficientes também são importantes. Prolog geralmente tende a fazer uma busca por força bruta, unificando termos para satisfazer uma consulta. Ainda não li a série inteira, então fico curioso se ela aborda esse problema.
Meu primeiro palpite é que isso seria algo como escrever as restrições conhecidas sobre a solução como uma espécie de “declaração de tipo”.