Approccio alla programmazione funzionale per un gioco semplificato utilizzando Scala e LWJGL

11

I, un programmatore imperativo Java, vorrebbe capire come generare una versione semplice di Space Invaders basata sui principi di progettazione della programmazione funzionale (in particolare la trasparenza referenziale). Tuttavia, ogni volta che cerco di pensare a un progetto, mi perdo nella palude di estrema mutabilità, la stessa mutevolezza che è evitata dai puristi della programmazione funzionale.

Come tentativo di apprendere la programmazione funzionale, ho deciso di provare a creare un gioco interattivo 2D molto semplice, Space Invader (notare la mancanza di plurale), in Scala utilizzando LWJGL . Ecco i requisiti per il gioco base:

  1. L'utente si trova nella parte inferiore dello schermo spostato a sinistra e a destra rispettivamente con i tasti "A" e "D"

  2. L'utente ha sparato proiettile puntato verso l'alto attivato dalla barra spaziatrice con una pausa minima tra i colpi per essere .5 secondi

  3. Il proiettile alieno sparato verso il basso è stato attivato da un tempo casuale di 0,5-1,5 secondi tra uno sparo e l'altro

Le cose intenzionalmente tralasciate dal gioco originale sono gli alieni WxH, le barriere di difesa degradabili x3, le navi con i dischi ad alta velocità nella parte superiore dello schermo.

Ok, ora al dominio del problema reale. Per me, tutte le parti deterministiche sono ovvie. Sono le parti non deterministiche che sembrano bloccare la mia capacità di considerare come avvicinarmi. Le parti deterministiche sono la traiettoria del proiettile una volta che esistono, il movimento continuo dell'alieno e l'esplosione a causa di un colpo su entrambi (o entrambi) della nave del giocatore o dell'alieno. Le parti non deterministiche (per me) gestiscono il flusso di input dell'utente, gestendo il recupero di un valore casuale per determinare i lanci di proiettili alieni e gestire l'output (sia grafica che audio).

Posso fare (e ho fatto) un sacco di questo tipo di sviluppo del gioco nel corso degli anni. Tuttavia, tutto era dal paradigma imperativo. E LWJGL fornisce anche una versione Java molto semplice di Space Invaders (di cui ho iniziato a spostarmi su Scala usando Scala come Java-senza-punto e virgola).

Ecco alcuni link che parlano di quest'area di cui nessuno sembra aver affrontato direttamente le idee in un modo che una persona proveniente dalla programmazione Java / Imperativa avrebbe capito:

  1. Retrogames puramente funzionali, parte 1 di James Hague

  2. post Overflow dello stack simile

  3. Clojure / Lisp Games

  4. Haskell Games on Stack Overflow

  5. Yampa's (in Haskell) Programmazione reattiva funzionale

Sembra che ci siano alcune idee nei giochi Clojure / Lisp e Haskell (con sorgente). Sfortunatamente, non sono in grado di leggere / interpretare il codice in modelli mentali che hanno alcun senso per il mio cervello imperativo Java, semplice e semplice.

Sono così eccitato dalle possibilità offerte da FP, posso semplicemente assaggiare le funzionalità di scalabilità multi-thread. Mi sento come se fossi in grado di ingannare come qualcosa di semplice come il tempo + evento + modello di casualità per Space Invader può essere implementato, separando le parti deterministiche e non deterministiche in un sistema ben progettato senza trasformarsi in ciò che sembra una teoria matematica avanzata ; cioè Yampa, sarei pronto. Se l'apprendimento del livello di teoria richiesto da Yampa per generare con successo giochi semplici è necessario, allora il sovraccarico di acquisire tutta la formazione necessaria e il quadro concettuale supererà di molto la mia comprensione dei benefici della FP (almeno per questo esperimento di apprendimento troppo semplificato ).

Qualsiasi feedback, modelli proposti, metodi suggeriti di approccio al dominio del problema (più specifici delle generalità coperte da James Hague) sarebbe molto apprezzato.

    
posta chaotic3quilibrium 24.01.2012 - 03:52
fonte

3 risposte

5

Un'implementazione idiomatica di Scala / LWJGL di Space Invaders non assomiglierebbe molto a un'implementazione di Haskell / OpenGL. Scrivere una implementazione Haskell potrebbe essere un esercizio migliore a mio parere. Ma se vuoi stare con Scala, ecco alcune idee su come scriverlo in stile funzionale.

Prova a usare solo oggetti immutabili. Potresti avere un oggetto Game che contiene un Player , un Set[Invader] (assicurati di usare immutable.Set ), ecc. Dare a Player an update(state: Game): Player (potrebbe anche prendere depressedKeys: Set[Int] , ecc.) e dare alle altre classi metodi simili.

Per casualità, scala.util.Random non è immutabile come Haskell System.Random , ma potresti creare il tuo generatore immutabile. Questo è inefficiente ma dimostra l'idea.

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

Per l'input e il rendering da tastiera / mouse, non c'è modo di chiamare le funzioni impure. Sono impuri anche in Haskell, sono solo incapsulati in IO ecc in modo che i tuoi oggetti funzione reali siano tecnicamente puri (non leggono o scrivono loro stessi stati, essi descrivono routine che fare, e il sistema di runtime esegue quelle routine).

Non inserire il codice I / O negli oggetti immutabili come Game , Player e Invader . Puoi dare il metodo Player a render , ma dovrebbe apparire come

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

Sfortunatamente questo non si adatta bene con LWJGL dato che è così basato sullo stato, ma potresti costruire le tue astrazioni su di esso. Potresti avere una classe ImmutableCanvas che contiene un AWT Canvas , e il suo blit (e altri metodi) potrebbero clonare il% co_de sottostante, passarlo a Canvas , quindi esegui il rendering e restituisci il nuovo Display.setParent (nel tuo wrapper immutabile).

Aggiornamento : qui c'è un codice Java che mostra come andrei su questo. (Avrei scritto quasi lo stesso codice in Scala, tranne per il fatto che un set immutabile è incorporato e alcuni for-each loop possono essere sostituiti con mappe o pieghe.) Ho creato un giocatore che si muove e spara proiettili, ma io non ha aggiunto nemici poiché il codice stava già diventando lungo. Ho fatto quasi tutto copy-on-write - Penso che questo sia il concetto più importante.

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) {}
    }
  }
}
    
risposta data 24.01.2012 - 06:01
fonte
4

Bene, stai ostacolando i tuoi sforzi usando LWJGL - nulla contro di esso, ma imporrà idiomi non funzionali.

La tua ricerca è in linea con ciò che consiglierei, comunque. Gli "eventi" sono ben supportati nella programmazione funzionale attraverso concetti come la programmazione reattiva funzionale o la programmazione del flusso di dati. Puoi provare Reactive , una libreria FRP per Scala, per vedere se può contenere i tuoi effetti collaterali.

Inoltre, prendi una pagina da Haskell: usa le monade per incapsulare / isolare gli effetti collaterali. Vedi stato e monadi IO.

    
risposta data 24.01.2012 - 13:47
fonte
3

The non-deterministic parts (to me) are handling the stream of user input ... handling the output (both graphics and sound).

Sì, IO è non-deterministico e "tutto su" effetti collaterali. Questo non è un problema in un linguaggio funzionale non puro come Scala.

handling fetching a random value for determining alien bullet firings

Puoi trattare l'output di un generatore di numeri pseudocasuali come una sequenza infinita ( Seq in Scala).

...

Dove, in particolare, vedi la necessità di mutabilità? Se posso anticipare, potresti pensare che i tuoi spiriti abbiano una posizione nello spazio che varia nel tempo. Potresti trovare utile pensare a "zippers" in un contesto di questo tipo: link

    
risposta data 24.01.2012 - 05:01
fonte

Leggi altre domande sui tag