Scala 및 LWJGL을 사용한 단순화 된 게임을위한 기능적 프로그래밍 접근법


11

Java 명령형 프로그래머 인 I는 Functional Programming 설계 원칙 (특히 참조 투명성)을 기반으로 간단한 버전의 Space Invader를 생성하는 방법을 이해하고 싶습니다. 그러나 디자인을 생각할 때마다 함수형 프로그래밍 순수 론자들에 의해 멈춰진 것과 같은 극도의 가변성이라는 쇠약에 빠져들게됩니다.

함수형 프로그래밍을 배우기 위해 LWJGL을 사용하여 Scala 에서 매우 간단한 2D 대화 형 게임 인 Space Invader (복수 부족)를 만들려고했습니다 . 기본 게임에 대한 요구 사항은 다음과 같습니다.

  1. 화면 하단의 사용자 선박은 각각 "A"와 "D"키로 좌우로 움직였습니다.

  2. 스페이스 바에 의해 발사 된 사격 총알이 발사 간격을 최소 0.5 초로하여 활성화되었습니다.

  3. 외계 선박 탄환은 발사 사이에 임의의 시간에 0.5 초에서 1.5 초까지 발사됩니다.

원래 게임에서 의도적으로 제외 된 것은 WxH 외계인, 분해 가능한 방어 장벽 x3, 화면 상단의 고속 접시 우주선입니다.

이제 실제 문제 영역으로 넘어가겠습니다. 나를 위해 모든 결정 론적 부분이 분명합니다. 접근 방법을 고려할 수있는 능력을 차단하는 것은 비 결정적 부분입니다. 결정적인 부분은 일단 존재하는 탄환의 궤도, 외계인의 지속적인 움직임 및 플레이어의 선박 또는 외계인 중 하나 (또는 ​​둘 다)에 의한 폭발입니다. 비 결정적 부분 (나에게)은 사용자 입력 스트림을 처리하고 외계 총알 발사를 결정하고 출력 (그래픽 및 사운드 모두)을 결정하기 위해 임의의 값을 가져 오는 것을 처리합니다.

수년에 걸쳐 이러한 유형의 게임 개발을 많이 할 수있었습니다. 그러나 그것은 모두 명령형 패러다임에서 비롯된 것입니다. 그리고 LWJGL 은 매우 간단한 Java 버전의 스페이스 인베이더 를 제공합니다 (스칼라를 세미콜론없이 자바로 사용하여 스칼라로 이동하기 시작했습니다).

다음은 Java / Imperative Programming에서 온 사람이 이해할 수있는 방식으로 아이디어를 직접 다루지 않은 것으로 보이는이 영역에 대한 몇 가지 링크입니다.

  1. James Hague의 순수하게 기능적인 레트로 게임, 1 부

  2. 유사한 스택 오버플로 포스트

  3. 클로저 / 리스프 게임

  4. 스택 오버플로의 Haskell 게임

  5. Yampa (하스켈)의 기능적 반응성 프로그래밍

Clojure / Lisp 및 Haskell 게임 (소스 포함)에 몇 가지 아이디어가있는 것으로 보입니다. 불행히도, 나는 단순한 자바 명령 뇌에 이해가되는 정신 모델로 코드를 읽거나 해석 할 수 없습니다.

FP가 제공 할 수있는 가능성에 매우 고무되어 있으며 멀티 스레드 확장 기능을 맛볼 수 있습니다. Space Invader의 시간 + 이벤트 + 임의성 모델처럼 단순한 것을 구현할 수있는 방법을 알 수 있다면 고급 수학 이론처럼 느껴지지 않고 올바르게 설계된 시스템에서 결정 론적 부분과 비결정론 적 부분을 분리합니다. ; 즉 Yampa, 나는 설정됩니다. Yampa가 간단한 게임을 성공적으로 생성하는 데 필요한 이론 수준을 배우는 것이 필요한 경우, 필요한 모든 교육 및 개념적 프레임 워크를 얻는 데 드는 오버 헤드가 FP의 이점에 대한 나의 이해보다 훨씬 큽니다 (적어도이 단순화 된 학습 실험의 경우) ).

모든 제안, 제안 된 모델, 문제 영역에 접근하는 방법 (제임스 헤이그가 다루는 일반보다 더 구체적인 방법)에 대해 크게 감사하겠습니다.


1
질문 자체에 필수적이 아니기 때문에 질문에 대한 귀하의 블로그 부분을 삭제했습니다. 기사를 작성할 때 후속 기사에 대한 링크를 자유롭게 포함하십시오.
yannis

@Yannis-알았습니다. Tyvm!
chaotic3quilibrium

당신은 스칼라를 요청했는데, 이것이 단지 주석입니다. Clojure의 동굴 은 악성 유사 FP 스타일을 구현하는 방법에 대한 관리 가능한 읽기입니다. 작성자가 테스트 할 수있는 세계의 스냅 샷을 반환하여 상태를 처리합니다. 꽤 괜찮은데. 어쩌면 당신은 게시물을 탐색하고 그의 구현의 일부가 스칼라로 쉽게 양도 될 수 있는지 볼 수 있습니다
IAE

답변:


5

Space Invaders의 관용 스칼라 / LJJGL 구현은 Haskell / OpenGL 구현과 크게 같지 않습니다. 하스켈 구현을 작성하는 것이 내 생각에 더 나은 운동 일 수 있습니다. 그러나 Scala를 고수하고 싶다면 기능적 스타일로 작성하는 방법에 대한 아이디어가 있습니다.

불변 개체 만 사용하십시오. 당신은 할 수 Game를 보유하고 개체를 Player, A는 Set[Invader](반드시 사용하는 immutable.Set등) 부여 (그것도 걸릴 수 등), 다른 클래스에게 유사한 방법을 제공합니다.Playerupdate(state: Game): PlayerdepressedKeys: Set[Int]

임의성을 위해 scala.util.RandomHaskell 's와 같이 System.Random불변성은 아니지만 자체 불변 발전기를 만들 수 있습니다. 이것은 비효율적이지만 아이디어를 보여줍니다.

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

키보드 / 마우스 입력 및 렌더링의 경우 불완전한 함수를 호출 할 방법이 없습니다. Haskell에서도 불완전하고 IO실제로 캡슐화되어 실제 함수 객체가 기술적으로 순수합니다 (상태를 읽거나 쓰지 않고 루틴을 설명 하고 런타임 시스템이 해당 루틴을 실행 함) .

그냥처럼 불변의 객체에 I에게 / O 코드를 넣지 마십시오 Game, Player그리고 Invader. 당신은 줄 수 방법을하지만 같이 보일 것입니다Playerrender

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

불행히도 LWJGL은 상태 기반이기 때문에 적합하지 않지만 그 위에 자체 추상화를 작성할 수 있습니다. 당신은 할 수 ImmutableCanvas가 AWT를 보유하고 클래스를 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 코드를 추가했는데 도움이 되나요? 코드가 이상하게 보이면 불변의 copy-on-write 클래스의 작은 예를 살펴 보겠습니다. 이것은 괜찮은 설명처럼 보입니다.
Daniel Lubarov

2
@ chaotic3quilibrium 그것은 단지 정상적인 식별자입니다. args코드에서 인수를 무시 하는 경우가 아니라 때때로 사용합니다 . 불필요한 혼란을 드려 죄송합니다.
다니엘 Lubarov

2
걱정 마. 나는 방금 그것을 가정하고 나아 갔다. 어제 동안 예제 코드로 연주했습니다. 나는 생각이 달려 있다고 생각합니다. 이제 다른 것을 놓치고 있는지 궁금합니다. 임시 객체의 수는 엄청납니다. 모든 틱은 GameState를 표시하는 프레임을 생성합니다. 그리고 이전 틱의 GameState에서 해당 GameState에 도달하려면 각각 이전 GameState에서 하나의 작은 조정으로 여러 개의 개입 GameState 인스턴스를 생성해야합니다.
chaotic3quilibrium

3
예, 꽤 낭비입니다. 나는 GameState~ 32 바이트이기 때문에 여러 개의 틱이 각 틱으로 만들어 지더라도 사본이 그렇게 비싸다고 생각하지 않습니다 . 그러나 ImmutableSet많은 총알이 동시에 살아 있으면 s를 복사하는 것이 비용이 많이들 수 있습니다. 문제를 줄이는 ImmutableSet것처럼 트리 구조로 바꿀 수 scala.collection.immutable.TreeSet있습니다.
다니엘 Lubarov

2
그리고 ImmutableImage수정 될 때 큰 래스터를 복사하기 때문에 더 나쁩니다. 우리도 그 문제를 줄이기 위해 할 수있는 일이 있지만, 렌더링 코드를 명령형 스타일로 작성하는 것이 가장 실용적이라고 생각합니다 (하스켈 프로그래머조차도 그렇게합니다).
다니엘 Lubarov

4

글쎄, 당신은 LWJGL을 사용하여 노력을 망치고 있습니다-그것에 반대하는 것은 아니지만 비 기능적인 관용구를 강요합니다.

그러나 귀하의 연구는 내가 권장하는 것과 일치합니다. "이벤트"는 기능적 반응성 프로그래밍 또는 데이터 흐름 프로그래밍과 같은 개념을 통해 기능적 프로그래밍에서 잘 지원됩니다. Scala의 FRP 라이브러리 인 Reactive를 사용해 부작용을 포함 할 수 있는지 확인할 수 있습니다.

또한 Haskell에서 모나드를 사용하여 부작용을 캡슐화 / 분리하십시오. 상태 및 IO 모나드를 참조하십시오.


귀하의 회신에 대한 Tyvm. Reactive에서 키보드 / 마우스 입력 및 그래픽 / 사운드 출력을 얻는 방법을 잘 모르겠습니다. 거기에 있고 그냥 놓치고 있습니까? 모나드 사용에 대한 당신의 언급에 관해서는-나는 지금 그들에 대해 배우고 있지만 여전히 모나드가 무엇인지 완전히 이해하지 못합니다.
chaotic3quilibrium

3

비 결정적 부분 (나에게)은 사용자 입력 스트림을 처리하고 있습니다 ... 출력 (그래픽과 사운드 모두)을 처리합니다.

예, IO는 비 결정적이며 "모든"부작용입니다. 스칼라와 같은 순수하지 않은 기능적 언어에서는 문제가되지 않습니다.

외계인 총알 발사를 결정하기 위해 무작위 값 가져 오기 처리

의사 난수 생성기의 출력을 무한 시퀀스 ( Seq스칼라) 로 취급 할 수 있습니다 .

...

특히 돌연변이가 필요한 곳은 어디입니까? 내가 예상 할 수 있다면, 당신의 스프라이트가 시간이 지남에 따라 공간에서 위치를 갖는 것으로 생각할 수 있습니다. 이러한 상황에서 "지퍼"에 대해 생각하는 것이 유용 할 수 있습니다. 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.