Revisión de GameLisp: un nuevo lenguaje para escribir juegos en Rust



Un programador que se firma con el seudónimo de Fleabit lleva seis meses desarrollando su lenguaje de programación . La pregunta surge de inmediato: ¿otro idioma? ¿Para qué?



Estos son sus argumentos:



  • – , , , . , garbage collection .
  • Rust : , , – enum- ; pattern matching ; , ; .. , Rust : « , »; ; /, , .
  • JavaScript, Lua, Python Ruby; Rust – , - , . , garbage collector, , – , GC , . GameLisp – , .
  • GameLisp, – , , . enum- Rust, , . "" , .


En primer lugar, la simplicidad de la sintaxis y la simplicidad del intérprete se toman de Lisp en GameLisp: la implementación de GameLisp junto con la "biblioteca estándar" ahora toma 36 KLOC, en comparación, por ejemplo, con 455 KLOC en Python. Por otro lado, en comparación con Lisp normal, GameLisp no tiene listas y mucho menos se centra en la programación funcional y los datos inmutables; en cambio, como la mayoría de los lenguajes de programación, GameLisp se centra en la programación imperativa orientada a objetos.

La sintaxis basada en Lisp puede ser abrumadora, pero te acostumbras rápidamente a escribir (.print console (+ 2 2)), etc. en lugar de console.print (2 + 2). Esta sintaxis es mucho más simple y flexible que en los lenguajes de scripting conocidos: la coma se considera un carácter de espacio en blanco y se puede usar para mejorar la legibilidad en cualquier parte del código; en lugar de dos tipos de corchetes {} (), solo se usan corchetes; la mayoría de los caracteres ASCII se pueden usar en caracteres, por lo que I ~ <3 ~ Lisp! ~ ^ _ ^ es un nombre válido para una función o variable; Innecesario; para separar operaciones, etc. Puedo decir que sin ninguna experiencia previa con Lisp, en solo un par de noches pude reescribir el clásico NIBBLES.BAS en GameLisp: http://atari.ruvds.com/nibbles.html



Todo lo que hay en la "biblioteca estándar" de GameLisp para E / S es una función prn para imprimir en la salida estándar; no funciona el teclado / mouse, no hay archivos, no hay gráficos, no hay sonido. Se supone que el propio usuario de GameLisp implementa en Rust todas aquellas herramientas de interfaz que son relevantes específicamente en su proyecto. Como ejemplo de un enlace de este tipo, se publica un motor minimalista para juegos de navegador en https://gamelisp.rs/playground/ usando wasm-bindgenque proporciona el código GameLisp con play: down?, play: pressed?, play: release?, play: mouse-x, play: mouse-y, play: fill y play: draw. Mi puerto de Nibbles usa el mismo motor; acabo de agregarle una función para reproducir sonido. Es interesante comparar los tamaños: el NIBBLES.BAS original era de 24 KB; mi puerto en GameLisp es 9KB; El archivo WebAssembly con el tiempo de ejecución compilado de Rust, el intérprete GameLisp y el código del juego es de 2,5 MB, y también viene con un enlace JavaScript de 11 KB generado por wasm-bindgen.



Junto con un motor minimalista en https://gamelisp.rs/playground/Se agregaron implementaciones de GameLisp de tres juegos clásicos: pong, tetris y sapper. Tetris y Minesweeper son más grandes y complejos que mi puerto de Nibbles, y hay mucho que aprender de su código.



Para demostrar las capacidades de GameLisp, he elegido dos ejemplos; el primero se refiere a las macros. En NIBBLES.BAS, los niveles son especificados por el bloque de línea SELECT CASE con bucles anidados:



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 estos bucles tienen una estructura similar, que se puede incluir en una macro:



(let-macro set-walls (range ..walls)
  `(do ~..(map (fn1
    `(forni (i ~..range) (set-wall ~.._))) walls)))

      
      





Con esta macro, la descripción de todos los niveles se reduce en cuatro y se acerca lo más posible a una descripción declarativa similar a 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)))
  ...
      
      





En un lenguaje sin macros, por ejemplo, en JavaScript, una implementación similar oscurecería la descripción completa de los niveles con 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 ejemplo muestra claramente cómo el código JavaScript está sobrecargado con varias palabras de función y puntuación, de las que puede prescindir.

Mi segundo ejemplo es sobre máquinas de estado. Mi implementación del juego tiene la siguiente estructura:



(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)))))
      
      





En cada fotograma (cuando se llama desde window.requestAnimationFrame), el motor del juego llama al método Game.update. Dentro de la clase Game, un autómata se define a partir de los estados Init-Level, Playing, Erase-Snake, Game-Over, cada uno de los cuales define el método de actualización a su manera. En el estado Reproducción, se definen cinco campos privados a los que no se puede acceder desde otros estados. Además, el estado de reproducción tiene un estado de pausa anidado, es decir, el juego puede estar en el estado de reproducción o en el estado de reproducción: pausa. El constructor del estado Paused imprime la línea correspondiente en la pantalla cada vez que pasa a este estado; el método de actualización, en este estado, comprueba si la tecla P se ha presionado de nuevo y, si se presiona y se suelta, sale del estado de pausa y vuelve al estado de reproducción "normal". El método de actualización del estado de reproducción maneja las pulsaciones de teclas,calcula la nueva posición de los jugadores, y si uno de ellos choca contra la pared, pasa al estado Game-Over o al estado Erase-Snake. El constructor del estado Erase-Snake es interesante porque toma como parámetro un enlace a una serpiente, que debe borrarse bellamente antes de reiniciar el nivel. Finalmente, para el estado Game-Over, el constructor muestra un mensaje correspondiente en la pantalla, y el método de actualización está vacío, lo que significa que independientemente de las teclas presionadas, no se dibujará nada nuevo en la pantalla y es imposible salir de este estado.Finalmente, para el estado Game-Over, el constructor muestra el mensaje correspondiente en la pantalla y el método de actualización está vacío, lo que significa que no importa qué teclas se presionen, no se dibujará nada nuevo en la pantalla y es imposible salir de este estado.Finalmente, para el estado Game-Over, el constructor muestra el mensaje correspondiente en la pantalla y el método de actualización está vacío, lo que significa que no importa qué teclas se presionen, no se dibujará nada nuevo en la pantalla y es imposible salir de este estado.



El juego podría implementarse de una manera similar en un lenguaje de programación clásico: la clase Game habría anidado las clases InitLevel, Playing, EraseSnake, GameOver, habría un campo currentState y el método Game.update delegaría la llamada a currentState.update. Dentro de la clase Playing, habría una clase Paused anidada y el método Playing.update a su vez delegaría la llamada al subobjeto. Las macros de biblioteca estándar ocultan la generación automática de campos currentState y métodos de delegación para que el desarrollador del juego vea implementaciones de estado significativas en lugar de su marco estándar.



En lugar de una máquina de estado, sería posible implementar Nibbles como un bucle:



while (lives>0) {
  InitLevel;
  while (prize<10) {
    Playing;
    if (dies) {
      EraseSnake;
      break;
    }
  }
}
GameOver;

      
      





Así es como se implementó el juego QBasic original. Para un motor de navegador, dicho bucle estaría envuelto en un generador con rendimiento después de renderizar cada fotograma, y ​​Game.update consistiría en una llamada a iter-next! ... Preferí la implementación como un autómata por dos razones: primero, así es como funciona la implementación de Tetris. que el autor de GameLisp cita como ejemplo; y en segundo lugar, no hay nada inusual en los generadores de GameLisp en comparación con otros lenguajes de programación. El objetivo principal de los autómatas es implementar los estados de los personajes del juego (esperar, atacar, huir, etc.), lo cual es imposible mediante un bucle dentro del generador. Un argumento adicional a favor de los autómatas es el aislamiento de los datos relacionados con cada uno de los estados entre sí.






All Articles