Eu, um programador imperativo em Java, gostaria de entender como gerar uma versão simples do Space Invaders com base nos princípios de design da Programação Funcional (em particular Transparência Referencial). No entanto, toda vez que tento pensar em um design, me perco no pântano da extrema mutabilidade, a mesma mutabilidade que é evitada pelos puristas da programação funcional.
Como uma tentativa de aprender a Programação Funcional, decidi tentar criar um jogo interativo 2D muito simples, o Space Invader (observe a falta de plural), em Scala, usando o LWJGL . Aqui estão os requisitos para o jogo básico:
O envio do usuário na parte inferior da tela foi movido para a esquerda e para a direita pelas teclas "A" e "D", respectivamente
A bala de envio do usuário disparada diretamente para cima ativada pela barra de espaço com uma pausa mínima entre os disparos para ser de 0,5 segundos
Bala de nave alienígena disparada para baixo ativada por um tempo aleatório de 0,5 a 1,5 segundos entre tiros
As coisas intencionalmente deixadas de fora do jogo original são alienígenas WxH, barreiras de defesa degradáveis x3, disco voador de alta velocidade na parte superior da tela.
Ok, agora para o domínio do problema real. Para mim, todas as partes determinísticas são óbvias. São as partes não determinísticas que parecem estar bloqueando minha capacidade de considerar como abordar. As partes determinísticas são a trajetória da bala, uma vez que existem, o movimento contínuo do alien e a explosão devido a um golpe em um (ou em ambos) do navio do jogador ou no alien. As partes não determinísticas (para mim) estão lidando com o fluxo de entrada do usuário, buscando um valor aleatório para determinar disparos de balas alienígenas e lidando com a saída (gráficos e som).
Eu posso fazer (e já fiz) muito desse tipo de desenvolvimento de jogo ao longo dos anos. No entanto, tudo isso era do paradigma imperativo. E o LWJGL ainda fornece uma versão Java muito simples dos invasores do Space (da qual comecei a mudar para o Scala usando o Scala como Java sem ponto e vírgula).
Aqui estão alguns links que falam sobre essa área, dos quais nenhum parece ter lidado diretamente com as idéias de uma maneira que uma pessoa proveniente da programação Java / Imperative entenderia:
Parece que existem algumas idéias nos jogos Clojure / Lisp e Haskell (com fonte). Infelizmente, não sou capaz de ler / interpretar o código em modelos mentais que fazem algum sentido para o meu cérebro imperativo Java simplório.
Estou tão empolgado com as possibilidades oferecidas pelo FP que posso apenas provar os recursos de escalabilidade multithread. Sinto como se fosse capaz de entender como algo tão simples como o modelo de tempo + evento + aleatoriedade para o Space Invader pode ser implementado, segregando as partes determinísticas e não determinísticas em um sistema projetado adequadamente, sem que isso se transforme no que parece uma teoria matemática avançada ; Yampa, eu estaria pronto. Se é necessário aprender o nível de teoria que o Yampa exige para gerar jogos simples com sucesso, a sobrecarga de adquirir todo o treinamento e a estrutura conceitual necessários superará amplamente a minha compreensão dos benefícios do FP (pelo menos para este experimento de aprendizagem simplificado) )
Qualquer feedback, modelos propostos, métodos sugeridos para abordar o domínio do problema (mais específicos do que as generalidades cobertas por James Hague) seriam muito apreciados.
fonte
Respostas:
Uma implementação idiomática de Scala / LWJGL do Space Invaders não se pareceria muito com uma implementação de Haskell / OpenGL. Escrever uma implementação Haskell pode ser um exercício melhor na minha opinião. Mas se você quiser ficar com o Scala, aqui estão algumas idéias de como escrevê-lo em estilo funcional.
Tente usar apenas objetos imutáveis. Você pode ter um
Game
objeto que contém aPlayer
, aSet[Invader]
(certifique-se de usarimmutable.Set
), etc. DêPlayer
umupdate(state: Game): Player
(também pode levardepressedKeys: Set[Int]
, etc.) e dê às outras classes métodos semelhantes.Por acaso,
scala.util.Random
não é imutável como o de HaskellSystem.Random
, mas você pode criar seu próprio gerador imutável. Este é ineficiente, mas demonstra a ideia.Para entrada e renderização de teclado / mouse, não há como evitar funções impuras. Eles também são impuros em Haskell, apenas encapsulados em
IO
etc., para que seus objetos de função reais sejam tecnicamente puros (eles não lêem ou escrevem eles mesmos, descrevem rotinas que o fazem e o sistema de tempo de execução executa essas rotinas) .Apenas não coloque código de E / S em seus objetos imutáveis como
Game
,Player
eInvader
. Você pode darPlayer
umrender
método, mas deve parecerInfelizmente, isso não se encaixa bem com o LWJGL, pois é tão baseado em estado, mas você pode criar suas próprias abstrações sobre ele. Você pode ter uma
ImmutableCanvas
classe que possui um AWTCanvas
e seusblit
(e outros métodos) podem clonar o subjacenteCanvas
, passá-lo paraDisplay.setParent
, executar a renderização e retornar o novoCanvas
(em seu invólucro imutável).Atualização : Aqui está um código Java mostrando como eu faria isso. (Eu teria escrito quase o mesmo código em Scala, exceto que um conjunto imutável está embutido e alguns para cada loops poderiam ser substituídos por mapas ou dobras.) Eu criei um jogador que se move e dispara balas, mas eu não adicionou inimigos, pois o código já estava ficando longo. Eu fiz praticamente tudo de copiar em escrever - acho que esse é o conceito mais importante.
fonte
args
se o código ignorar argumentos. Desculpe pela confusão desnecessária.GameState
cópias seriam tão caras, mesmo que várias sejam feitas a cada tick, já que são ~ 32 bytes cada. Mas copiar osImmutableSet
s pode ser caro se muitas balas estiverem vivas ao mesmo tempo. Poderíamos substituirImmutableSet
por uma estrutura em árvorescala.collection.immutable.TreeSet
para diminuir o problema.ImmutableImage
é ainda pior, pois copia uma grande varredura quando é modificada. Existem algumas coisas que poderíamos fazer para diminuir esse problema também, mas acho que seria mais prático escrever código de renderização em estilo imperativo (mesmo os programadores Haskell normalmente o fazem).Bem, você está restringindo seus esforços usando o LWJGL - nada contra, mas isso impõe expressões não funcionais.
Sua pesquisa está alinhada com o que eu recomendaria, no entanto. Os "eventos" são bem suportados na programação funcional por meio de conceitos como programação reativa funcional ou programação de fluxo de dados. Você pode experimentar o Reactive , uma biblioteca de FRP para Scala, para ver se ele pode conter seus efeitos colaterais.
Além disso, tire uma página do Haskell: use mônadas para encapsular / isolar efeitos colaterais. Veja mônadas de estado e IO.
fonte
Sim, as E / S não são determinísticas e têm efeitos colaterais "tudo sobre". Isso não é um problema em uma linguagem funcional não pura como o Scala.
Você pode tratar a saída de um gerador de números pseudoaleatórios como uma sequência infinita (
Seq
em Scala)....
Onde, em particular, você vê a necessidade de mutabilidade? Se eu puder antecipar, você pode pensar em seus sprites como tendo uma posição no espaço que varia ao longo do tempo. Você pode achar útil pensar em "zíperes" nesse contexto: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php
fonte