Phương pháp lập trình chức năng cho một trò chơi đơn giản hóa bằng Scala và LWJGL


11

Tôi, một lập trình viên mệnh lệnh Java, muốn hiểu cách tạo một phiên bản đơn giản của Kẻ xâm lược không gian dựa trên các nguyên tắc thiết kế Lập trình chức năng (cụ thể là Tính minh bạch tham chiếu). Tuy nhiên, mỗi lần tôi cố gắng nghĩ về một thiết kế, tôi lại lạc vào sự biến đổi cực đoan, khả năng biến đổi tương tự bị các nhà thuần túy lập trình chức năng xa lánh.

Như một nỗ lực để học Lập trình chức năng, tôi quyết định thử tạo một trò chơi tương tác 2D rất đơn giản, Space Invader (lưu ý việc thiếu số nhiều), trong Scala bằng cách sử dụng LWJGL . Dưới đây là các yêu cầu cho trò chơi cơ bản:

  1. Người dùng giao hàng ở dưới cùng của màn hình di chuyển sang trái và phải bằng các phím "A" và "D" tương ứng

  2. Viên đạn tàu người dùng bắn thẳng lên được kích hoạt bởi thanh không gian với khoảng dừng tối thiểu giữa các lần bắn là 0,5 giây

  3. Đạn tàu ngoài hành tinh bắn thẳng xuống được kích hoạt trong khoảng thời gian ngẫu nhiên từ 0,5 đến 1,5 giây giữa các lần bắn

Những thứ cố tình rời khỏi trò chơi gốc là người ngoài hành tinh WxH, hàng rào phòng thủ có thể phân hủy x3, tàu đĩa tốc độ cao ở đầu màn hình.

Được rồi, bây giờ đến miền vấn đề thực tế. Đối với tôi, tất cả các phần xác định là rõ ràng. Đó là những phần không xác định dường như đang chặn khả năng của tôi để xem xét cách tiếp cận. Các phần xác định là quỹ đạo của viên đạn một khi chúng tồn tại, chuyển động liên tục của người ngoài hành tinh và vụ nổ do một cú đánh vào một trong hai (hoặc cả hai) con tàu của người chơi hoặc người ngoài hành tinh. Các phần không xác định (với tôi) đang xử lý luồng đầu vào của người dùng, xử lý tìm nạp một giá trị ngẫu nhiên để xác định các phát đạn của người ngoài hành tinh và xử lý đầu ra (cả đồ họa và âm thanh).

Tôi có thể làm (và đã làm) rất nhiều kiểu phát triển trò chơi này trong những năm qua. Tuy nhiên, tất cả đều từ mô hình mệnh lệnh. Và LWJGL thậm chí còn cung cấp một phiên bản Java rất đơn giản cho những kẻ xâm lược không gian (trong đó tôi bắt đầu chuyển sang Scala bằng Scala dưới dạng Java - không có dấu chấm phẩy).

Dưới đây là một số liên kết nói về lĩnh vực này mà dường như không ai trực tiếp xử lý các ý tưởng theo cách mà một người đến từ lập trình Java / mệnh lệnh sẽ hiểu:

  1. Retrogames hoàn toàn chức năng, Phần 1 của James Hague

  2. Bài viết Stack Overflow tương tự

  3. Trò chơi Clojure / Lisp

  4. Trò chơi Haskell trên Stack Overflow

  5. Lập trình phản ứng chức năng của Yampa (trong Haskell)

Dường như có một số ý tưởng trong các trò chơi Clojure / Lisp và Haskell (có nguồn). Thật không may, tôi không thể đọc / giải thích mã thành các mô hình tinh thần có ý nghĩa đối với bộ não mệnh lệnh Java đơn giản của tôi.

Tôi rất hào hứng với các khả năng do FP cung cấp, tôi chỉ có thể nếm thử các khả năng mở rộng đa luồng. Tôi cảm thấy như thể có thể tìm hiểu làm thế nào một cái gì đó đơn giản như mô hình thời gian + sự kiện + ngẫu nhiên cho Space Invader có thể được thực hiện, tách biệt các phần xác định và không xác định trong một hệ thống được thiết kế phù hợp mà không biến thành lý thuyết toán học tiên tiến ; tức là Yampa, tôi sẽ được thiết lập. Nếu việc học ở cấp độ lý thuyết Yampa dường như cần phải tạo thành công các trò chơi đơn giản là cần thiết, thì chi phí để có được tất cả các khung đào tạo và khái niệm cần thiết sẽ vượt xa sự hiểu biết của tôi về lợi ích của FP (ít nhất là cho thí nghiệm học tập quá đơn giản này ).

Bất kỳ phản hồi, mô hình đề xuất, phương pháp đề xuất tiếp cận miền vấn đề (cụ thể hơn các khái quát được đề cập bởi James Hague) sẽ được đánh giá cao.


1
Tôi đã xóa phần về blog của bạn khỏi câu hỏi, vì nó không cần thiết cho chính câu hỏi. Vui lòng bao gồm một liên kết đến một bài viết tiếp theo khi bạn đi xung quanh để viết nó.
yannis

@Yannis - Hiểu rồi Tyvm!
hỗn loạn3quilibrium

Bạn đã hỏi Scala, đó là lý do tại sao đây chỉ là một nhận xét. Hang động Clojure là một cách đọc có thể quản lý được về cách thực hiện kiểu roguelike FP. Nó xử lý trạng thái bằng cách trả về một ảnh chụp nhanh về thế giới mà tác giả có thể kiểm tra. Điều đó thật tuyệt. Có lẽ bạn có thể duyệt qua các bài đăng và xem liệu bất kỳ phần nào trong quá trình thực hiện của anh ấy có thể dễ dàng chuyển sang Scala
IAE

Câu trả lời:


5

Một triển khai Scala / LWJGL thành công của Kẻ xâm lược không gian sẽ trông giống như triển khai Haskell / OpenGL. Viết một triển khai Haskell có thể là một bài tập tốt hơn theo ý kiến ​​của tôi. Nhưng nếu bạn muốn gắn bó với Scala, đây là một số ý tưởng về cách viết nó theo phong cách chức năng.

Cố gắng chỉ sử dụng các đối tượng bất biến. Bạn có thể có một Gameđối tượng chứa a Player, a Set[Invader](chắc chắn sử dụng immutable.Set), v.v. Đưa ra Playermột update(state: Game): Player(nó cũng có thể lấy depressedKeys: Set[Int], v.v.) và đưa ra các lớp khác các phương thức tương tự.

Đối với sự ngẫu nhiên, scala.util.Randomkhông phải là bất biến như của Haskell System.Random, nhưng bạn có thể tạo ra trình tạo không thể thay đổi của riêng bạn. Điều này là không hiệu quả nhưng nó thể hiện ý tưởng.

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

Đối với đầu vào và kết xuất bàn phím / chuột, không có cách nào để gọi các hàm không tinh khiết. Họ đang bất tịnh trong Haskell quá, chúng tôi chỉ gói gọn trong IOvv để đối tượng chức năng thực tế của bạn là tinh khiết về mặt kỹ thuật (họ không đọc hoặc ghi tình trạng bản thân, họ mô tả thói quen mà làm, và các hệ thống thời gian chạy thực hiện những thói quen) .

Chỉ cần không đặt mã I / O trong các đối tượng bất biến của bạn như Game, PlayerInvader. Bạn có thể đưa ra Playermột renderphương thức, nhưng nó sẽ giống như

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

Thật không may, điều này không phù hợp với LWJGL vì nó dựa trên trạng thái, nhưng bạn có thể xây dựng các bản tóm tắt của riêng mình trên đầu trang. Bạn có thể có một ImmutableCanvaslớp chứa AWT Canvasvà nó blit(và các phương thức khác) có thể sao chép bên dưới Canvas, chuyển nó vào Display.setParent, sau đó thực hiện kết xuất và trả về cái mới Canvas(trong trình bao bọc bất biến của bạn).


Cập nhật : Đây là một số mã Java cho thấy tôi sẽ làm thế nào về điều này. (Tôi đã viết gần như cùng một mã trong Scala, ngoại trừ một bộ bất biến được tích hợp sẵn và một vài vòng cho mỗi vòng có thể được thay thế bằng bản đồ hoặc nếp gấp.) đã không thêm kẻ thù vì mã đã được lâu. Tôi đã thực hiện mọi thứ về copy-on-write - tôi nghĩ đây là khái niệm quan trọng nhất.

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
Tôi đã thêm một số mã Java - nó có giúp gì không? Nếu mã trông lạ, tôi sẽ xem xét một số ví dụ nhỏ hơn về các lớp sao chép, bất biến. Điều này có vẻ như một lời giải thích đàng hoàng.
Daniel Lubarov

2
@ chaotic3quilibrium nó chỉ là một định danh bình thường. Đôi khi tôi sử dụng nó thay vì argsnếu mã bỏ qua các đối số. Xin lỗi vì sự nhầm lẫn không cần thiết.
Daniel Lubarov

2
Đừng lo lắng. Tôi chỉ giả định điều đó và di chuyển trên. Tôi đã chơi với mã ví dụ của bạn trong khi ngày hôm qua. Tôi nghĩ rằng tôi có ý tưởng. Bây giờ, nó làm tôi tự hỏi nếu tôi thiếu một cái gì đó khác. Số lượng các đối tượng tạm thời là humong. Mỗi đánh dấu sẽ tạo ra một khung hiển thị GameState. Và để truy cập GameState đó từ GameState của tick trước đó liên quan đến việc tạo ra một số phiên bản GameState can thiệp, mỗi trường hợp có một điều chỉnh nhỏ từ GameState trước đó.
hỗn loạn3quilibrium

3
Vâng, nó khá lãng phí. Tôi không nghĩ rằng các GameStatebản sao sẽ tốn kém như vậy, mặc dù nhiều bản được tạo ra mỗi dấu, vì chúng là ~ 32 byte mỗi bản. Nhưng sao chép ImmutableSets có thể tốn kém nếu nhiều viên đạn còn sống cùng một lúc. Chúng ta có thể thay thế ImmutableSetbằng một cấu trúc cây như scala.collection.immutable.TreeSetđể giảm bớt vấn đề.
Daniel Lubarov

2
ImmutableImagethậm chí còn tệ hơn, vì nó sao chép một raster lớn khi nó được sửa đổi. Có một số điều chúng ta có thể làm để giảm bớt vấn đề đó, nhưng tôi nghĩ sẽ tốt nhất nếu chỉ viết mã kết xuất theo kiểu bắt buộc (ngay cả các lập trình viên Haskell thường làm như vậy).
Daniel Lubarov

4

Chà, bạn đang cản trở những nỗ lực của mình bằng cách sử dụng LWJGL - không có gì chống lại nó, nhưng nó sẽ áp đặt các thành ngữ phi chức năng.

Tuy nhiên, nghiên cứu của bạn phù hợp với những gì tôi muốn giới thiệu. "Sự kiện" được hỗ trợ tốt trong lập trình chức năng thông qua các khái niệm như lập trình phản ứng chức năng hoặc lập trình dataflow. Bạn có thể dùng thử Reactive , thư viện FRP cho Scala, để xem nó có thể chứa tác dụng phụ của bạn không.

Ngoài ra, lấy một trang ra khỏi Haskell: sử dụng các đơn nguyên để đóng gói / cô lập các tác dụng phụ. Xem các đơn vị nhà nước và IO.


Tyvm cho câu trả lời của bạn. Tôi không chắc chắn làm thế nào để có được đầu vào bàn phím / chuột và đầu ra đồ họa / âm thanh từ Reactive. Có nó ở đó và tôi chỉ thiếu nó? Theo tài liệu tham khảo của bạn về việc sử dụng một đơn nguyên - tôi mới tìm hiểu về chúng và vẫn chưa hoàn toàn hiểu thế nào là một đơn nguyên.
hỗn loạn3quilibrium

3

Các phần không xác định (với tôi) đang xử lý luồng đầu vào của người dùng ... xử lý đầu ra (cả đồ họa và âm thanh).

Có, IO không mang tính quyết định và tác dụng phụ "tất cả về". Đó không phải là vấn đề trong một ngôn ngữ chức năng không thuần túy như Scala.

xử lý lấy một giá trị ngẫu nhiên để xác định các phát đạn của người ngoài hành tinh

Bạn có thể coi đầu ra của trình tạo số giả ngẫu nhiên là một chuỗi vô hạn ( Seqtrong Scala).

...

Cụ thể, bạn thấy sự cần thiết của tính đột biến ở đâu? Nếu tôi có thể dự đoán, bạn có thể nghĩ rằng các họa tiết của bạn có một vị trí trong không gian thay đổi theo thời gian. Bạn có thể thấy hữu ích khi nghĩ về "khóa kéo" trong ngữ cảnh như vậy: http://sciencebloss.com/goodmath/2010/01/zippers_making_feftal_upda.php


Tôi thậm chí không biết cách cấu trúc mã ban đầu để nó là lập trình chức năng thành ngữ. Sau đó, tôi không hiểu kỹ thuật chính xác (hoặc ưa thích) để thêm vào mã "không trong sạch". Tôi biết rằng tôi có thể sử dụng Scala như "Java không có dấu chấm phẩy". Tôi không muốn làm điều đó. Tôi muốn tìm hiểu làm thế nào FP giải quyết một môi trường năng động rất đơn giản mà không phụ thuộc vào rò rỉ thời gian hoặc giá trị. Điều đó có ý nghĩa?
hỗn loạn3quilibrium
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.