web-dev-qa-db-ja.com

ScalaおよびLWJGLを使用した単純化されたゲームの関数型プログラミングアプローチ

私は、Java命令型プログラマーであり、関数型プログラミングの設計原則(特に参照の透明性)に基づいてスペースインベーダーのシンプルなバージョンを生成する方法を理解したいと考えています。しかし、毎回ある設計の場合、関数型プログラミングの純粋主義者たちによって避けられたものと同じ、非常に可変性のある泥沼に迷い込んでしまいます。

関数型プログラミングを学ぶ試みとして、私は Scala[〜#〜を使用して、非常にシンプルな2Dインタラクティブゲーム、Space Invader(複数形がないことに注意)を作成することを試みることにしました。 ] lwjgl [〜#〜] 。基本的なゲームの要件は次のとおりです。

  1. 画面の下部にあるユーザーの出荷は、それぞれ「A」キーと「D」キーで左右に移動しました

  2. ユーザー船の弾丸は、スペースバーによってアクティブになり、ショット間の最小休止時間は.5秒です。

  3. エイリアンの船の弾丸が発砲の間に0.5〜1.5秒のランダムな時間で発動し、まっすぐに発砲

元のゲームから意図的に除外されているのは、WxHエイリアン、分解可能な防御バリアx3、画面上部の高速受け皿船です。

さて、実際の問題領域に移りましょう。私にとって、決定論的な部分はすべて明白です。アプローチ方法を検討する私の能力を妨げているように見えるのは、非決定論的な部分です。確定的な部分とは、存在する弾丸の軌道、エイリアンの連続的な動き、プレイヤーの船またはエイリアンのいずれか(または両方)へのヒットによる爆発です。 (私にとって)非決定的な部分は、ユーザー入力のストリームの処理、エイリアンの弾丸の発射を決定するためのランダムな値のフェッチの処理、および出力(グラフィックとサウンドの両方)の処理です。

私はこのタイプのゲーム開発を何年にもわたって行うことができます(そして行ってきました)。しかし、それはすべて命令パラダイムからのものでした。そして [〜#〜] lwjgl [〜#〜] も非常に単純な JavaバージョンのSpace invaders (そのうち私はScala使用Scala Javaなしのセミコロンとして)。

この領域について説明しているリンクの一部を以下に示します。Java/ Imperativeプログラミングから来た人が理解するような方法でアイデアを直接扱った人はいません。

  1. Purely Functional Retrogames、Part 1 by James Hague

  2. 同様のスタックオーバーフローの投稿

  3. Clojure/LISPゲーム

  4. スタックオーバーフローのHaskellゲーム

  5. Yampaの(Haskellで)関数型リアクティブプログラミング

Clojure/LISPおよびHaskellゲーム(ソース付き)にはいくつかのアイデアがあるようです。残念ながら、私は単純なJava命令型の頭脳にとって意味のあるメンタルモデルにコードを読み取って解釈することができません。

FPが提供する可能性にとても興奮しています。マルチスレッドのスケーラビリティ機能を試してみることができます。 Space Invaderの時間+イベント+ランダム性モデルのような単純なものを実装して、適切に設計されたシステムで決定論的部分と非決定論的部分を分離して、高度な数学理論のようなものに変換する方法を理解できたと思います;つまり、ヤンパ、私は設定されます。 Yampaが単純なゲームを正常に生成するために必要な理論レベルの学習が必要である場合、必要なすべてのトレーニングと概念フレームワークを取得するオーバーヘッドは、FP(少なくともこの単純化された学習実験では)。

フィードバック、モデルの提案、問題領域へのアプローチ方法(ジェームズハーグがカバーする一般性よりも具体的)があれば、大歓迎です。

11

Space InvadersのScala/LWJGLの慣用的な実装は、Haskell/OpenGLの実装とあまり似ていません。私の意見では、Haskellの実装を作成する方がよいかもしれません。しかし、Scalaを使い続けたい場合は、機能的なスタイルでScalaを記述するためのアイデアをいくつか示します。

不変オブジェクトのみを使用するようにしてください。 Gameを保持するPlayerオブジェクト、_Set[Invader]_(必ず _immutable.Set_ を使用するなど)を使用できます。Playerupdate(state: Game): Player(it _depressedKeys: Set[Int]_など)を取り、他のクラスに同様のメソッドを提供することもできます。

ランダム性については、 _scala.util.Random_ はHaskellの _System.Random_ のように不変ではありませんが、独自の不変のジェネレーターを作成できます。これは非効率的ですが、アイデアを示しています。

_case class ImmutablePRNG(val seed: Long) extends Immutable {
    lazy val nextLong: (Long, ImmutableRNG) =
        (seed, ImmutablePRNG(new Random(seed).nextLong()))
    ...
}
_

キーボード/マウスの入力とレンダリングの場合、不純な関数を呼び出す方法はありません。それらはHaskellでも不純であり、IOなどにカプセル化されているだけなので、実際の関数オブジェクトは技術的に純粋です(状態自体を読み書きしない、記述実行するルーチン、およびランタイムシステムがそれらのルーチンを実行します)。

GamePlayerInvaderなどの不変オブジェクトにI/Oコードを配置しないでください。 Playerrenderメソッドを指定できますが、次のようになります。

_render(state: Game, buffer: Image): Image
_

残念ながら、これは状態ベースであるため、LWJGLには適していませんが、その上に独自の抽象化を構築できます。あなたはAWTを保持するImmutableCanvasクラスを持つことができます Canvas 、そしてそのblit(および他のメソッド)は基礎となるCanvasを複製し、それを _Display.setParent_ に渡してから、レンダリングを実行し、新しいCanvasを(不変のラッパーで)返します。


更新:ここにいくつかのJavaコードが私がこれについてどうやって行くかを示しています。(私はほぼ同じように書いたでしょう) Scalaのコード。ただし、不変セットが組み込まれており、for-eachループをマップまたはフォールドに置き換えることができます。)移動して弾丸を発射するプレーヤーを作成しましたが、コード以降は敵を追加しませんでした。 copy-on-write –これは最も重要なコンセプトだと思います。

_import Java.awt.*;
import Java.awt.geom.*;
import Java.awt.image.*;
import Java.awt.event.*;
import javax.swing.*;
import Java.util.*;

import static Java.awt.event.KeyEvent.*;

// An immutable wrapper around a Set. Doesn't implement Set or Collection
// because that would require quite a bit of code.
class ImmutableSet<T> implements Iterable<T> {
  final Set<T> backingSet;

  // Construct an empty set.
  ImmutableSet() {
    backingSet = new HashSet<T>();
  }

  // Copy constructor.
  ImmutableSet(ImmutableSet<T> src) {
    backingSet = new HashSet<T>(src.backingSet);
  }

  // Return a new set with an element added.
  ImmutableSet<T> plus(T elem) {
    ImmutableSet<T> copy = new ImmutableSet<T>(this);
    copy.backingSet.add(elem);
    return copy;
  }

  // Return a new set with an element removed.
  ImmutableSet<T> minus(T elem) {
    ImmutableSet<T> copy = new ImmutableSet<T>(this);
    copy.backingSet.remove(elem);
    return copy;
  }

  boolean contains(T elem) {
    return backingSet.contains(elem);
  }

  @Override public Iterator<T> iterator() {
    return backingSet.iterator();
  }
}

// An immutable, copy-on-write wrapper around BufferedImage.
class ImmutableImage {
  final BufferedImage backingImage;

  // Construct a blank image.
  ImmutableImage(int w, int h) {
    backingImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
  }

  // Copy constructor.
  ImmutableImage(ImmutableImage src) {
    backingImage = new BufferedImage(
        src.backingImage.getColorModel(),
        src.backingImage.copyData(null),
        false, null);
  }

  // Clear the image.
  ImmutableImage clear(Color c) {
    ImmutableImage copy = new ImmutableImage(this);
    Graphics g = copy.backingImage.getGraphics();
    g.setColor(c);
    g.fillRect(0, 0, backingImage.getWidth(), backingImage.getHeight());
    return copy;
  }

  // Draw a filled circle.
  ImmutableImage fillCircle(int x, int y, int r, Color c) {
    ImmutableImage copy = new ImmutableImage(this);
    Graphics g = copy.backingImage.getGraphics();
    g.setColor(c);
    g.fillOval(x - r, y - r, r * 2, r * 2);
    return copy;
  }
}

// An immutable, copy-on-write object describing the player.
class Player {
  final int x, y;
  final int ticksUntilFire;

  Player(int x, int y, int ticksUntilFire) {
    this.x = x;
    this.y = y;
    this.ticksUntilFire = ticksUntilFire;
  }

  // Construct a player at the starting position, ready to fire.
  Player() {
    this(SpaceInvaders.W / 2, SpaceInvaders.H - 50, 0);
  }

  // Update the game state (repeatedly called for each game tick).
  GameState update(GameState currentState) {
    // Update the player's position based on which keys are down.
    int newX = x;
    if (currentState.keyboard.isDown(VK_LEFT) || currentState.keyboard.isDown(VK_A))
      newX -= 2;
    if (currentState.keyboard.isDown(VK_RIGHT) || currentState.keyboard.isDown(VK_D))
      newX += 2;

    // Update the time until the player can fire.
    int newTicksUntilFire = ticksUntilFire;
    if (newTicksUntilFire > 0)
      --newTicksUntilFire;

    // Replace the old player with an updated player.
    Player newPlayer = new Player(newX, y, newTicksUntilFire);
    return currentState.setPlayer(newPlayer);
  }

  // Update the game state in response to a key press.
  GameState keyPressed(GameState currentState, int key) {
    if (key == VK_SPACE && ticksUntilFire == 0) {
      // Fire a bullet.
      Bullet b = new Bullet(x, y);
      ImmutableSet<Bullet> newBullets = currentState.bullets.plus(b);
      currentState = currentState.setBullets(newBullets);

      // Make the player wait 25 ticks before firing again.
      currentState = currentState.setPlayer(new Player(x, y, 25));
    }
    return currentState;
  }

  ImmutableImage render(ImmutableImage img) {
    return img.fillCircle(x, y, 20, Color.RED);
  }
}

// An immutable, copy-on-write object describing a bullet.
class Bullet {
  final int x, y;
  static final int radius = 5;

  Bullet(int x, int y) {
    this.x = x;
    this.y = y;
  }

  // Update the game state (repeatedly called for each game tick).
  GameState update(GameState currentState) {
    ImmutableSet<Bullet> bullets = currentState.bullets;
    bullets = bullets.minus(this);
    if (y + radius >= 0)
      // Add a copy of the bullet which has moved up the screen slightly.
      bullets = bullets.plus(new Bullet(x, y - 5));
    return currentState.setBullets(bullets);
  }

  ImmutableImage render(ImmutableImage img) {
    return img.fillCircle(x, y, radius, Color.BLACK);
  }
}

// An immutable, copy-on-write snapshot of the keyboard state at some time.
class KeyboardState {
  final ImmutableSet<Integer> depressedKeys;

  KeyboardState(ImmutableSet<Integer> depressedKeys) {
    this.depressedKeys = depressedKeys;
  }

  KeyboardState() {
    this(new ImmutableSet<Integer>());
  }

  GameState keyPressed(GameState currentState, int key) {
    return currentState.setKeyboard(new KeyboardState(depressedKeys.plus(key)));
  }

  GameState keyReleased(GameState currentState, int key) {
    return currentState.setKeyboard(new KeyboardState(depressedKeys.minus(key)));
  }

  boolean isDown(int key) {
    return depressedKeys.contains(key);
  }
}

// An immutable, copy-on-write description of the entire game state.
class GameState {
  final Player player;
  final ImmutableSet<Bullet> bullets;
  final KeyboardState keyboard;

  GameState(Player player, ImmutableSet<Bullet> bullets, KeyboardState keyboard) {
    this.player = player;
    this.bullets = bullets;
    this.keyboard = keyboard;
  }

  GameState() {
    this(new Player(), new ImmutableSet<Bullet>(), new KeyboardState());
  }

  GameState setPlayer(Player newPlayer) {
    return new GameState(newPlayer, bullets, keyboard);
  }

  GameState setBullets(ImmutableSet<Bullet> newBullets) {
    return new GameState(player, newBullets, keyboard);
  }

  GameState setKeyboard(KeyboardState newKeyboard) {
    return new GameState(player, bullets, newKeyboard);
  }

  // Update the game state (repeatedly called for each game tick).
  GameState update() {
    GameState current = this;
    current = current.player.update(current);
    for (Bullet b : current.bullets)
      current = b.update(current);
    return current;
  }

  // Update the game state in response to a key press.
  GameState keyPressed(int key) {
    GameState current = this;
    current = keyboard.keyPressed(current, key);
    current = player.keyPressed(current, key);
    return current;
  }

  // Update the game state in response to a key release.
  GameState keyReleased(int key) {
    GameState current = this;
    current = keyboard.keyReleased(current, key);
    return current;
  }

  ImmutableImage render() {
    ImmutableImage img = new ImmutableImage(SpaceInvaders.W, SpaceInvaders.H);
    img = img.clear(Color.BLUE);
    img = player.render(img);
    for (Bullet b : bullets)
      img = b.render(img);
    return img;
  }
}

public class SpaceInvaders {
  static final int W = 640, H = 480;

  static GameState currentState = new GameState();

  public static void main(String[] _) {
    JFrame frame = new JFrame() {{
      setSize(W, H);
      setTitle("Space Invaders");
      setContentPane(new JPanel() {
        @Override public void paintComponent(Graphics g) {
          BufferedImage img = SpaceInvaders.currentState.render().backingImage;
          ((Graphics2D) g).drawRenderedImage(img, new AffineTransform());
        }
      });
      addKeyListener(new KeyAdapter() {
        @Override public void keyPressed(KeyEvent e) {
          currentState = currentState.keyPressed(e.getKeyCode());
        }
        @Override public void keyReleased(KeyEvent e) {
          currentState = currentState.keyReleased(e.getKeyCode());
        }
      });
      setLocationByPlatform(true);
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setVisible(true);
    }};

    for (;;) {
      currentState = currentState.update();
      frame.repaint();
      try {
        Thread.sleep(20);
      } catch (InterruptedException e) {}
    }
  }
}
_
5
Daniel Lubarov

まあ、あなたはLWJGLを使ってあなたの努力を妨害しています-それに対して何もありませんが、それは非機能的なイディオムを課します。

しかし、あなたの研究は私がお勧めするものと一致しています。 「イベント」は、関数型リアクティブプログラミングやデータフロープログラミングなどの概念を通じて、関数型プログラミングで十分にサポートされています。 ScalaのFRPライブラリである Reactive を試して、副作用が含まれていないか確認してください。

また、Haskellからページを取り出します。モナドを使用して、副作用をカプセル化/分離します。状態とIOモナドを参照してください。

4

(私にとって)非決定的な部分は、ユーザー入力のストリームを処理しています...出力(グラフィックスとサウンドの両方)を処理しています。

はい、IOは非決定的で「すべて」の副作用です。これはScalaなどの純粋でない関数型言語では問題ではありません。

エイリアンの弾の発砲を決定するためのランダムな値のフェッチの処理

疑似乱数ジェネレータの出力を無限シーケンス(ScalaではSeq)として扱うことができます。

...

特に、可変性の必要性はどこにあると思いますか?私が予想するかもしれませんが、あなたのスプライトは、時間とともに変化する空間内の位置を持っていると考えるかもしれません。このようなコンテキストで「ジッパー」について考えると便利な場合があります。 http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php

3
Larry OBrien