Um programador que se assina com o pseudônimo Fleabit vem desenvolvendo sua linguagem de programação há seis meses . A questão surge imediatamente: outro idioma? Pelo que?
Aqui estão seus argumentos:
- – , , , . , garbage collection .
- Rust : , , – enum- ; pattern matching ; , ; .. , Rust : « , »; ; /, , .
- JavaScript, Lua, Python Ruby; Rust – , - , . , garbage collector, , – , GC , . GameLisp – , .
- GameLisp, – , , . enum- Rust, , . "" , .
Em primeiro lugar, a simplicidade da sintaxe e a simplicidade do interpretador são tiradas do Lisp em GameLisp: a implementação de GameLisp junto com a "biblioteca padrão" agora leva 36 KLOC, comparados, por exemplo, com 455 KLOC em Python. Por outro lado, em comparação com Lisp regular, GameLisp não tem listas e muito menos foco em programação funcional e dados imutáveis; em vez disso, como a maioria das linguagens de script, GameLisp concentra-se na programação orientada a objetos imperativa.
A sintaxe baseada em Lisp pode ser opressora, mas você rapidamente se acostuma a escrever (.print console (+ 2 2)), etc. em vez de console.print (2 + 2). Essa sintaxe é muito mais simples e flexível do que em linguagens de script familiares: a vírgula é considerada um caractere de espaço em branco e pode ser usada para melhorar a legibilidade em qualquer parte do código; em vez de dois tipos de colchetes {} (), apenas colchetes são usados; a maioria dos caracteres ASCII pode ser usada em caracteres, então I ~ <3 ~ Lisp! ~ ^ _ ^ é um nome válido para uma função ou variável; Não é necessário; para separar operações, etc. Posso dizer que sem nenhuma experiência anterior com Lisp, em apenas algumas noites fui capaz de reescrever o clássico NIBBLES.BAS no GameLisp: http://atari.ruvds.com/nibbles.html
Tudo o que existe na "biblioteca padrão" do GameLisp para E / S é uma função prn para imprimir em stdout; nenhum teclado / mouse funciona, nenhum arquivo, nenhum gráfico, nenhum som. Presume-se que o próprio usuário do GameLisp implemente no Rust todas as ferramentas de interface que são relevantes especificamente em seu projeto. Como exemplo de tal ligação, um mecanismo minimalista para jogos de navegador está postado em https://gamelisp.rs/playground/ usando wasm-bindgenque fornece o código GameLisp com as funções play: down?, play: pressionado?, play: lançado?, play: mouse-x, play: mouse-y, play: fill e play: draw. Minha porta de Nibbles usa o mesmo mecanismo - acabei de adicionar uma função a ela para reproduzir som. É interessante comparar os tamanhos: o NIBBLES.BAS original tinha 24 KB; minha porta no GameLisp é de 9 KB; O arquivo WebAssembly com o tempo de execução Rust compilado, o interpretador GameLisp e o código do jogo tem 2,5 MB e também vem com uma ligação JavaScript de 11 KB gerada por wasm-bindgen.
Junto com um motor minimalista em https://gamelisp.rs/playground/Adicionadas implementações de GameLisp de três jogos clássicos: pong, tetris e sapper. Tetris e Minesweeper são maiores e mais complexos do que meu porte de Nibbles, e há muito o que aprender com seu código.
Para demonstrar as capacidades do GameLisp, escolhi dois exemplos; o primeiro diz respeito a macros. Em NIBBLES.BAS, os níveis são especificados pelo bloco de linha SELECT CASE com loops aninhados:
SELECT CASE curLevel
CASE 1
sammy(1).row = 25: sammy(2).row = 25
sammy(1).col = 50: sammy(2).col = 30
sammy(1).direction = 4: sammy(2).direction = 3
CASE 2
FOR i = 20 TO 60
Set 25, i, colorTable(3)
NEXT i
sammy(1).row = 7: sammy(2).row = 43
sammy(1).col = 60: sammy(2).col = 20
sammy(1).direction = 3: sammy(2).direction = 4
CASE 3
FOR i = 10 TO 40
Set i, 20, colorTable(3)
Set i, 60, colorTable(3)
NEXT i
sammy(1).row = 25: sammy(2).row = 25
sammy(1).col = 50: sammy(2).col = 30
sammy(1).direction = 1: sammy(2).direction = 2
...
Todos esses loops têm uma estrutura semelhante, que pode ser incluída em uma macro:
(let-macro set-walls (range ..walls)
`(do ~..(map (fn1
`(forni (i ~..range) (set-wall ~.._))) walls)))
Com essa macro, a descrição de todos os níveis é reduzida em quatro e se torna o mais próximo possível de uma descrição declarativa do tipo JSON:
(match @level
(1 (set-locations '(25 50 right) '(25 30 left)))
(2 (set-walls (20 60) (25 i))
(set-locations '(7 60 left) '(43 20 right)))
(3 (set-walls (10 40) (i 20) (i 60))
(set-locations '(25 50 up) '(25 30 down)))
...
Em uma linguagem sem macros - por exemplo, em JavaScript - uma implementação semelhante obscureceria toda a descrição dos níveis com lambdas:
switch (level) {
case 1: setLocations([25, 50, "right"], [25, 30, "left"]); break;
case 2: setWalls([20, 60], i => [25, i]);
setLocations([7, 60, "left"], [43, 20, "right"]); break;
case 3: setWalls([10, 40], i => [i, 20], i => [i, 60]);
setLocations([25, 50, "up"], [25, 30, "down"]); break;
...
Este exemplo mostra claramente como o código JavaScript está sobrecarregado com várias palavras de pontuação e função, das quais você pode dispensar.
Meu segundo exemplo é sobre máquinas de estado. Minha implementação do jogo tem a seguinte estrutura:
(defclass Game
...
(fsm
(state Playing
(field blink-rate (Rate 0.2))
(field blink-on)
(field move-rate (Rate 0.3))
(field target)
(field prize 1)
(state Paused
(init-state ()
(@center "*** PAUSED ***" 0))
(wrap Playing:update (dt)
(when (play:released? 'p)
(@center " LEVEL {@level} " 0)
(@disab! 'Paused))))
(met update (dt)
...
(when (play:released? 'p)
(@enab! 'Paused) (return))
...
; Move the snakes
(.at @move-rate dt (fn0
(for snake in @snakes (when (> [snake 'lives] 0)
(let position (clone [[snake 'body] 0]))
...
; If player runs into any point, he dies
(when (@occupied? position)
(play:sound 'die)
(dec! [snake 'lives])
(dec! [snake 'score] 10)
(if (all? (fn1 (== 0 [_ 'lives])) @snakes)
(@enab! 'Game-Over)
(@enab! 'Erase-Snake snake))
(return))
...
(state Game-Over
(init-state ()
(play:fill ..(@screen-coords 10 (-> @grid-width (/ 2) (- 16))) ..(@screen-coords 7 32) 255 255 255)
(play:fill ..(@screen-coords 11 (-> @grid-width (/ 2) (- 15))) ..(@screen-coords 5 30) ..@background)
(@center "G A M E O V E R" 13))
(met update (dt)))))
Em cada quadro (quando chamado de window.requestAnimationFrame), o mecanismo de jogo chama o método Game.update. Dentro da classe Game, um autômato é definido a partir dos estados Init-Level, Playing, Erase-Snake, Game-Over, cada um dos quais define o método de atualização em sua própria maneira. No estado Playing, cinco campos privados são definidos que não podem ser acessados de outros estados. Além disso, o estado de Reprodução tem um estado Pausado aninhado, ou seja, o jogo pode estar no estado Jogando ou no estado Jogando: Pausado. O construtor de estado Pausado imprime a linha apropriada na tela cada vez que faz a transição para este estado; o método de atualização, neste estado, verifica se a tecla P foi pressionada novamente e, se pressionada e liberada, sai do estado Pausado, retornando ao estado de Reprodução "normal". O método de atualização do estado de reprodução lida com pressionamentos de tecla,calcula a nova posição dos jogadores, e se um deles colidir com a parede, ele vai para o estado Game-Over ou para o estado Erase-Snake. O construtor do estado Erase-Snake é interessante porque toma como parâmetro um link para uma cobra, que deve ser perfeitamente apagado antes de reiniciar o nível. Por fim, para o estado Game-Over, o construtor exibe uma mensagem correspondente na tela, e o método de atualização fica vazio, o que significa que independentemente das teclas pressionadas, nada de novo será desenhado na tela, sendo impossível sair desse estado.Por fim, para o estado Game-Over, o construtor exibe uma mensagem correspondente na tela, e o método de atualização fica vazio, o que significa que independentemente das teclas pressionadas, nada de novo será desenhado na tela, sendo impossível sair desse estado.Por fim, para o estado Game-Over, o construtor exibe uma mensagem correspondente na tela, e o método de atualização fica vazio, o que significa que independentemente das teclas pressionadas, nada de novo será desenhado na tela, sendo impossível sair desse estado.
O jogo poderia ser implementado de maneira semelhante em uma linguagem de script clássica: a classe Game teria aninhado as classes InitLevel, Playing, EraseSnake, GameOver, haveria um campo currentState e o método Game.update delegaria a chamada para currentState.update. Dentro da classe Playing, haveria uma classe Paused aninhada e o método Playing.update, por sua vez, delegaria a chamada ao subobjeto. As macros de biblioteca padrão ocultam a geração automática de campos currentState e métodos de delegação para que o desenvolvedor do jogo veja a implementação significativa de estados, em vez de seu clichê.
Em vez de uma máquina de estado, o Nibbles poderia ser implementado como um loop:
while (lives>0) {
InitLevel;
while (prize<10) {
Playing;
if (dies) {
EraseSnake;
break;
}
}
}
GameOver;
É assim que o jogo QBasic original foi implementado. Para um mecanismo de navegador, tal loop seria envolvido em um gerador com rendimento após renderizar cada quadro, e Game.update consistiria em uma chamada para iter-next! .. Eu preferia a implementação como um autômato por dois motivos: primeiro, é assim que funciona a implementação do Tetris. que o autor de GameLisp cita como exemplo; e segundo, não há nada incomum sobre os geradores no GameLisp em comparação com outras linguagens de script. O principal objetivo dos autômatos é implementar os estados dos personagens do jogo (esperando, atacando, fugindo, etc.), o que é impossível por meio de um loop dentro do gerador. Um argumento adicional a favor dos autômatos é o isolamento dos dados relacionados a cada um dos estados.