ScalaとLWJGLを使用した簡易ゲームの関数型プログラミングアプローチ


11

Javaの命令型プログラマであるIは、関数型プログラミングの設計原則(特に参照の透明性)に基づいて、スペースインベーダーの単純なバージョンを生成する方法を理解したいと考えています。しかし、デザインを考えようとするたびに、極端な可変性の泥沼で迷子になります。これは、関数型プログラミングの純粋主義者によって回避される同じ可変性です。

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

  1. 「A」キーと「D」キーでそれぞれ画面の下部にあるユーザーシップを左右に移動しました

  2. 発射間隔が0.5秒になるようにスペースバーで起動したユーザー船の弾丸を真っ直ぐ発射

  3. 射撃の間に0.5秒から1.5秒のランダムな時間で起動するエイリアン船の弾丸が真下に発射されました

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

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

私はこの種のゲーム開発を長年にわたって行うことができます(そして、これまでしてきました)。しかし、それはすべて命令型パラダイムからでした。さらに、LWJGLは、スペース侵略者の非常にシンプルなJavaバージョンを提供します(これは、セミコロンなしのJavaとしてScalaを使用してScalaに移行し始めました)。

Java / Imperativeプログラミングから来た人が理解する方法でアイデアを直接扱ったものはいないように思われる、この領域に関するいくつかのリンクを以下に示します。

  1. 純粋に機能的なレトロゲーム、パート1ジェームズハーグ

  2. 同様のスタックオーバーフローポスト

  3. Clojure / Lispゲーム

  4. スタックオーバーフローに関するHaskellゲーム

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

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

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

フィードバック、提案されたモデル、問題領域へのアプローチ方法(ジェームズハーグがカバーしている一般性よりも具体的)をお勧めします。


1
あなたのブログに関する部分を質問から削除しました。質問自体には不可欠ではないからです。記事を書き始める際に、フォローアップ記事へのリンクを自由に含めてください。
ヤニス

@Yannis-わかった。Tyvm!
chaotic3quilibrium

あなたはScalaを要求しました。これがコメントに過ぎない理由です。Caves of Clojureは、ローグライクFPスタイルの実装方法に関する管理可能な読み物です。作成者がテストできる世界のスナップショットを返すことで状態を処理します。それはいいね。たぶん、あなたは記事を拾い読みし、彼の実装のいずれかの部分がスカラ座にも容易に譲渡されているかどうかを確認することができます
IAE

回答:


5

Space Invadersの慣用的なScala / LWJGL実装は、Haskell / OpenGL実装のようには見えません。私の意見では、Haskellの実装を作成する方がよいでしょう。ただし、Scalaを使い続けたい場合は、機能的なスタイルでScalaを作成するためのアイデアをいくつか紹介します。

不変オブジェクトのみを使用してください。あなたが持っている可能性がGame保持しているオブジェクトをPlayerSet[Invader](必ず使用することimmutable.Setなど、)を得(それも取ることができるなど、)、および他のクラスに同様の方法を与えます。Playerupdate(state: Game): PlayerdepressedKeys: 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()))
    ...
}

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

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

render(state: Game, buffer: Image): Image

残念ながら、これは状態ベースなのでLWJGLにはうまく適合しませんが、その上に独自の抽象化を構築できます。ImmutableCanvasAWTを保持するクラスを作成し、Canvasそのblit(および他のメソッド)で基になるクローンを作成し、CanvasそれをDisplay.setParentに渡し、レンダリングを実行し、新しいCanvas(不変のラッパーで)を返すことができます。


更新:ここに、私がこれをどうするかを示すJavaコードがあります。(不変セットが組み込まれ、いくつかのfor-eachループをマップまたはフォールドに置き換えることができることを除いて、Scalaでほぼ同じコードを記述していました。)動き回って弾丸を発射するプレーヤーを作成しましたが、コードがすでに長くなっているため、敵を追加しませんでした。コピーオンライトのすべてを作成しました。これが最も重要な概念だと思います。

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) {}
    }
  }
}

2
いくつかのJavaコードを追加しました-それは役立ちますか?コードが奇妙に見える場合は、不変のコピーオンライトクラスの小さな例を見てみましょう。これはまともな説明のように見えます。
ダニエルルバロフ

2
@ chaotic3quilibriumそれは単なる普通の識別子です。argsコードが引数を無視する場合の代わりに使用することがあります。不必要な混乱のためにすみません。
ダニエルルバロフ

2
心配ない。私はそれを想定して先に進みました。昨日しばらくの間、サンプルコードを使用しました。アイデアのコツがあると思います。今、私は何か他のものが欠けているのではないかと思っています。一時オブジェクトの数は膨大です。ティックごとにGameStateを表示するフレームが生成されます。そして、前のティックのGameStateからそのGameStateに到達するには、多数の介在するGameStateインスタンスを生成し、それぞれに前のGameStateからの1つの小さな調整を行います。
chaotic3quilibrium

3
ええ、それはかなり無駄です。GameStateコピーはそれぞれティックごとに作成されますが、それらはそれぞれ〜32バイトなので、コピーはそれほど高価ではないと思います。しかしImmutableSet、多くの弾丸が同時に生きている場合、のコピーは高価になる可能性があります。問題を軽減するImmutableSetようなツリー構造に置き換えることができscala.collection.immutable.TreeSetます。
ダニエルルバロフ

2
そして、ImmutableImageそれをコピーそれは修正の大規模なラスタいるので、さらに悪くなります。この問題を軽減するためにできることもいくつかありますが、単に命令型スタイルでレンダリングコードを書くことが最も実用的だと思います(Haskellプログラマーでさえ通常そうします)。
ダニエルルバロフ

4

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

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

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


返信用のTyvm。Reactiveからキーボード/マウス入力とグラフィックス/サウンド出力を取得する方法がわかりません。それはそこにあり、私はちょうどそれを見逃していますか?モナドの使用に関するあなたの参照について-私は今それらについて学んでいますが、それでもモナドが何であるかを完全には理解していません。
chaotic3quilibrium

3

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

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

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

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

...

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


イディオム関数プログラミングになるように初期コードを構成する方法すら知りません。その後、「不純な」コードを追加するための正しい(または望ましい)テクニックがわかりません。私はScalaを「セミコロンのないJava」として使用できることを知っています。そんなことしたくない。時間または値の可変性リークに依存することなく、FPが非常に単純な動的環境にどのように対処するかを学びたいと思います。それは理にかなっていますか?
chaotic3quilibrium
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.