GameLispレビュー:Rustでゲームを書くための新しい言語



仮名Fleabitで署名したプログラマーは、6か月間プログラミング言語を開発してきました すぐに疑問が生じます:別の言語?何のために?



ここに彼の議論があります:



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


まず、構文の単純さとインタープリターの単純さは、GameLispのLispから取得されます。GameLispと「標準ライブラリ」の実装は、たとえば、Pythonの455 KLOCと比較して、36KLOCを必要とします。一方、通常のLispと比較すると、GameLispにはリストがなく、機能プログラミングと不変データに重点​​が置かれていません。代わりに、ほとんどのスクリプト言語と同様に、GameLispは必須のオブジェクト指向プログラミングに重点を置いています。

Lispベースの構文は圧倒される可能性がありますが、console.print(2 + 2)の代わりに(.print console(+ 2 2))などを書くことにすぐに慣れます。この構文は、使い慣れたスクリプト言語よりもはるかに単純で柔軟性があります。コンマは空白文字と見なされ、コード内のどこでも読みやすさを向上させるために使用できます。 2種類のブラケット{}()の代わりに、丸いブラケットのみが使用されます。ほとんどのASCII文字は文字で使用できるため、I〜 <3〜Lisp!〜^ _ ^は関数または変数の有効な名前です。必要ありません;操作を分離するなど。 Lispの過去の経験がなくても、ほんの2、3晩で、GameLispのクラシックなNIBBLES.BASを書き直すことができました http://atari.ruvds.com/nibbles.html



GameLispにあるI / Oの「標準ライブラリ」は、stdoutに出力するためのprn関数だけです。キーボード/マウスの動作、ファイル、グラフィック、サウンドはありません。 GameLispユーザー自身が、自分のプロジェクトに特に関連するすべてのインターフェイスツールをRustに実装していると想定されています。このようなバインディングの例として、ブラウザゲーム用の最小限のエンジンがwasm-bindgenを使用してhttps://gamelisp.rs/playground/に投稿さ れてい ます。これは、GameLispコードにplay:down?、play:pressed?、play:released?、play:mouse-x、play:mouse-y、play:fill、play:drawを提供します。 Nibblesの私のポートは同じエンジンを使用しています-サウンドを再生する機能を追加しただけです。サイズを比較するのは興味深いことです。元のNIBBLES.BASは24KBでした。 GameLispの私のポートは9KBです。コンパイルされたRustランタイム、GameLispインタープリター、およびゲームコードを含むWebAssemblyファイルは2.5MBであり、wasm-bindgenによって生成された11KBのJavaScriptバインディングも付属しています。 https://gamelisp.rs/playground/で



ミニマルなエンジンと一緒に 3つの古典的なゲームのGameLisp実装を追加しました:ポン、テトリス、サッパー。TetrisとMinesweeperは、私の移植版であるNibblesよりも大きく複雑であり、それらのコードから学ぶことはたくさんあります。



GameLispの機能を示すために、2つの例を選択しました。最初はマクロに関するものです。NIBBLES.BASでは、レベルはネストされたループを持つSELECTCASEラインブロックによって指定されます。



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

...
      
      





これらのループはすべて同様の構造を持っており、マクロに含めることができます。



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

      
      





このマクロを使用すると、すべてのレベルの説明が4分の1に削減され、宣言的な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)))
  ...
      
      





マクロのない言語(JavaScriptなど)では、同様の実装により、ラムダを含むレベルの説明全体がわかりにくくなります。



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;
...
      
      





この例は、JavaScriptコードがさまざまな句読点や関数ワードでオーバーロードされていることを明確に示しています。

私の2番目の例は、ステートマシンについてです。ゲームの私の実装は次の構造を持っています:



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





すべてのフレームで(window.requestAnimationFrameから呼び出された場合)、ゲームエンジンはGame.updateメソッドを呼び出します。 Gameクラス内では、オートマトンはInit-Level、Playing、Erase-Snake、Game-Overの各状態から定義され、それぞれが独自の方法で更新メソッドを定義します。再生状態では、他の状態からアクセスできない5つのプライベートフィールドが定義されています。さらに、再生状態にはネストされた一時停止状態があります。ゲームは、再生状態または再生:一時停止状態のいずれかになります。 Paused状態コンストラクターは、この状態に遷移するたびに、対応する行を画面に出力します。 updateメソッドは、この状態で、Pキーが再度押されたかどうかを確認し、押して放すと、一時停止状態を終了し、「プレーン」再生状態に戻ります。再生状態の更新メソッドは、キーストロークを処理します。プレーヤーの新しい位置を計算し、そのうちの1人が壁に衝突した場合、ゲームオーバー状態または消去スネーク状態になります。 Erase-Snake状態のコンストラクターは、レベルを再開する前に美しく消去する必要があるスネークへのリンクをパラメーターとして受け取るという点で興味深いものです。最後に、ゲームオーバー状態の場合、コンストラクターは対応するメッセージを画面に表示し、更新メソッドは空です。つまり、どのキーを押しても画面に新しいものは描画されず、この状態を終了することはできません。最後に、ゲームオーバー状態の場合、コンストラクターは対応するメッセージを画面に表示し、更新メソッドは空です。つまり、押されたキーに関係なく、画面に新しいものは何も描画されず、この状態を終了することはできません。最後に、ゲームオーバー状態の場合、コンストラクターは対応するメッセージを画面に表示し、更新メソッドは空です。つまり、どのキーを押しても画面に新しいものは描画されず、この状態を終了することはできません。



ゲームは、従来のスクリプト言語でも同様の方法で実装できます。GameクラスにはInitLevel、Playing、EraseSnake、GameOverクラスがネストされ、currentStateフィールドがあり、Game.updateメソッドはcurrentState.updateへの呼び出しを委任します。 Playingクラス内には、ネストされたPausedクラスがあり、Playing.updateメソッドは呼び出しをサブオブジェクトに委任します。標準のライブラリマクロは、currentStateフィールドと委任メソッドの自動生成を非表示にして、ゲーム開発者が定型文ではなく、状態の意味のある実装を確認できるようにします。



ステートマシンの代わりに、ニブルをループとして実装できます。



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

      
      





これは、元のQBasicゲームが実装された方法です。ブラウザエンジンの場合、このようなループは各フレームのレンダリング後にyieldでジェネレータにラップされ、Game.updateはiter-nextの呼び出しで構成されます!.. 2つの理由から、オートマトンとしての実装を好みました。1つは、これがTetris実装の動作方法です。 GameLispの作者が例として引用しています。そして第二に、他のスクリプト言語と比較して、GameLispのジェネレーターに異常はありません。オートマトンの主な目的は、ゲームキャラクターの状態(待機、攻撃、逃走など)を実装することです。これは、ジェネレーター内のループでは不可能です。オートマトンを支持する追加の議論は、各状態に関連するデータを互いに分離することです。






All Articles