Scala ve LWJGL kullanarak basitleştirilmiş bir oyun için Fonksiyonel Programlama yaklaşımı


11

Java zorunlu bir programcısı olarak, Fonksiyonel Programlama tasarım ilkelerine (özellikle Referans Şeffaflığı) dayalı Space Invaders'ın basit bir versiyonunun nasıl oluşturulacağını anlamak istiyorum. Bununla birlikte, bir tasarımı her düşünmeye çalıştığımda, aşırı değişkenliğin, işlevsel programlama safları tarafından kapatılan aynı mutabilitenin tacizinde kayboluyorum.

Fonksiyonel Programlamayı öğrenmek için, Scala'da LWJGL kullanarak çok basit bir 2D interaktif oyun olan Space Invader (çoğul eksikliğine dikkat edin) yaratmaya karar verdim . Temel oyun için gereksinimler şunlardır:

  1. Ekranın altındaki kullanıcı gemisi sırasıyla "A" ve "D" tuşları ile sola ve sağa taşındı

  2. Kullanıcı gemi mermisi, atışlar arasında minimum duraklama ile .5 saniye olacak şekilde boşluk çubuğu tarafından etkinleştirildiğinde doğrudan ateşlenir

  3. Yabancı gemi mermi, atışlar arasında .5 ila 1.5 saniye arasında rastgele bir süre ile etkinleştirildi.

Orijinal oyundan kasıtlı olarak dışarıda bırakılan şeyler, ekranın üstündeki WxH uzaylıları, parçalanabilir savunma bariyerleri x3, yüksek hızlı daire gemisi.

Tamam, şimdi asıl problem alanına. Benim için, tüm deterministik parçalar açıktır. Nasıl yaklaşacağımı düşünme yeteneğimi engelleyen deterministik olmayan kısımlar. Deterministik parçalar, merminin yörüngesi bir kez var olduklarında, uzaylının sürekli hareketi ve oyuncunun gemisinin veya uzaylının (ya da her ikisinin) bir vuruşundan dolayı meydana gelen patlamadır. Deterministik olmayan parçalar (benim için) kullanıcı girişi akışını idare ediyor, uzaylı mermi atışlarını belirlemek ve çıktıyı (hem grafik hem de ses) işlemek için rastgele bir değer alıyor.

Yıllar boyunca bu tür oyun geliştirmelerinin çoğunu yapabilirim (ve yaptım). Ancak, hepsi zorunlu paradigmadan kaynaklanıyordu. Ve LWJGL bile Space invaders'ın çok basit bir Java sürümünü sunuyor (bunların arasında Scala'ya noktalı virgül olmadan Java kullanarak Scala'ya geçmeye başladım).

Java / Imperative programlamasından gelen bir kişinin anlayacağı şekilde fikirlerle doğrudan ilgilenmemiş gibi görünen bu alan hakkında konuşan bazı bağlantılar:

  1. Tamamen Fonksiyonel Retro Oyunlar, Bölüm 1 James Hague

  2. Benzer Yığın Taşma direği

  3. Clojure / Lisp Oyunları

  4. Yığın Taşması Üzerindeki Haskell Oyunları

  5. Yampa (Haskell'de) Fonksiyonel Reaktif Programlama

Görünüşe göre Clojure / Lisp ve Haskell oyunlarında (kaynakla birlikte) bazı fikirler var. Maalesef, kodu basitleştirilmiş Java zorunluluklı beynim için anlamlı olan zihinsel modellere okuyamıyorum / yorumlayamıyorum.

FP'nin sunduğu imkanlardan dolayı çok heyecanlıyım, sadece çok iş parçacıklı ölçeklenebilirlik yeteneklerinin tadına bakabilirim. Space Invader için zaman + olay + rastgelelik modeli kadar basit bir şeyin ne kadar basit bir şekilde uygulanabileceğini, gelişmiş matematiksel teori gibi hissettiren düzgün tasarlanmış bir sistemde deterministik ve deterministik olmayan kısımları ayırabiliyordum. ; yani Yampa, hazır olurdum. Teori düzeyini öğrenmek Yampa'nın başarılı bir şekilde basit oyunlar üretmesini gerektiriyor gibi görünüyorsa, gerekli tüm eğitim ve kavramsal çerçeveyi elde etme yükü, FP'nin yararları hakkındaki anlayışımdan daha ağır basacaktır (en azından bu aşırı basitleştirilmiş öğrenme deneyi için) ).

Herhangi bir geri bildirim, önerilen modeller, sorun alanına yaklaşma yöntemleri önerildi (James Hague tarafından kapsanan genellemelerden daha spesifik) çok takdir edilecektir.


1
Blogunuzla ilgili kısmı sorudan kaldırdım, çünkü sorunun kendisi için gerekli değildi. Yazmaya geldiğinizde bir takip makalesine bağlantı eklemekten çekinmeyin.
yannis

@Yannis - Anladım. Tyvm!
chaotic3quilibrium

Scala'yı istedin, bu yüzden bu sadece bir yorum. Clojure Mağaraları, roguelike bir FP stilinin nasıl uygulanacağı konusunda yönetilebilir bir okuma yapıyor. Daha sonra yazarın test edebileceği dünyanın bir anlık görüntüsünü döndürerek durumu işler. Bu oldukça havalı. Belki yayınlara göz atabilir ve uygulamasının herhangi bir bölümünün Scala
IAE

Yanıtlar:


5

Space Invaders'ın deyimsel bir Scala / LWJGL uygulaması, Haskell / OpenGL uygulaması gibi görünmeyecektir. Bence bir Haskell uygulaması yazmak daha iyi bir uygulama olabilir. Ancak Scala'ya bağlı kalmak istiyorsanız, işte onu fonksiyonel tarzda nasıl yazacağınıza dair bazı fikirler.

Yalnızca sabit nesneleri kullanmaya çalışın. Bir olabilirdi Gamebir tutan bir nesne Player, bir Set[Invader](emin kullanmak için immutable.Setvs.) verin Playerbir update(state: Game): Player(o da sürebilir depressedKeys: Set[Int]vb) ve diğer sınıflara benzer yöntemler verir.

Rastgelelik için, scala.util.RandomHaskell gibi değişmez değildir System.Random, ancak kendi immmutable jeneratör yapabiliriz. Bu verimsiz ama fikri gösteriyor.

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

Klavye / fare girişi ve oluşturma için saf olmayan işlevleri çağırmanın bir yolu yoktur. Haskell'de de saf değiller, sadece kapsüllenmişler, IOböylece gerçek işlev nesneleriniz teknik olarak saftır (kendilerini okumaz veya yazmazlar, bunu yapan rutinleri tanımlarlar ve çalışma zamanı sistemi bu rutinleri yürütürler) .

Sadece G / Ç kodunu Game, Playerve gibi değişmez nesnelerinize koymayın Invader. PlayerBir renderyöntem verebilirsiniz , ancak şöyle görünmelidir

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

Ne yazık ki bu LWJGL ile çok iyi uyuşmuyor, çünkü devlete kendi soyutlamalarınızı oluşturabilirsiniz. ImmutableCanvasBir AWT tutan bir sınıfa sahip olabilirsiniz Canvasve onun blit(ve diğer yöntemleri) temelini klonlayabilir Canvas, aktarabilir Display.setParent, ardından oluşturmayı gerçekleştirebilir ve yenisini Canvas(değişmez paketinizde) döndürebilir .


Güncelleme : İşte bu konuda nasıl yapacağımı gösteren bazı Java kodları. (Scala'da neredeyse aynı kodu yazardım, ancak değişmez bir set yerleşik ve her biri için birkaç döngü harita veya kıvrımlarla değiştirilebilir.) Etrafta hareket eden ve mermi ateşleyen bir oyuncu yaptım, ama ben kod çoktan uzandığından düşman eklemedi. Her şeyi kopyalamak üzerine yazdım - bence bu en önemli kavram.

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
Bazı Java kodları ekledim - yardımcı oluyor mu? Kod garip görünüyorsa, değişmez, yazma üzerine kopyalama sınıflarının bazı küçük örneklerine bakarım. Bu iyi bir açıklama gibi görünüyor.
Daniel Lubarov

2
@ chaotic3quilibrium sadece normal bir tanımlayıcıdır. argsKod argümanları yoksayarsa bazen kullanın . Gereksiz karışıklık için özür dilerim.
Daniel Lubarov

2
Telaşa gerek yok. Ben sadece bunu kabul ettim ve devam ettim. Dün için örnek kodunuzla oynadım. Sanırım bu fikre kapıldım. Şimdi, başka bir şey eksik mi diye merak ediyorum. Geçici nesnelerin sayısı çoktur. Her onay işareti bir GameState görüntüleyen bir kare oluşturur. Ve önceki kene GameState'ten bu GameState'e ulaşmak için, her biri bir önceki GameState'ten küçük bir ayarlamayla, araya giren bir dizi GameState örneği oluşturmayı içerir.
chaotic3quilibrium

3
Evet, oldukça savurgan. GameStateHer biri birkaç onay yapılmış olsa da , kopyaların bu kadar pahalı olacağını düşünmüyorum , çünkü her biri ~ 32 bayt. Ancak ImmutableSetaynı anda birçok mermi yaşıyorsa , s kopyalamak pahalı olabilir. Sorunu azaltmak için ImmutableSetbir ağaç yapısıyla değiştirebiliriz scala.collection.immutable.TreeSet.
Daniel Lubarov

2
Ve ImmutableImagedaha da kötüsü, çünkü değiştirildiğinde büyük bir raster kopyalar. Bu sorunu da azaltmak için yapabileceğimiz bazı şeyler var, ancak bence sadece oluşturma kodunu zorunlu bir tarzda yazmak en pratik olacaktır (Haskell programcıları bile normalde bunu yapar).
Daniel Lubarov

4

LWJGL kullanarak çabalarınızı hamstringliyorsunuz - buna karşı hiçbir şey yok, ancak fonksiyonel olmayan deyimler dayatacak.

Ancak araştırmanız tavsiye ettiğim şeye uygun. "Olaylar", fonksiyonel reaktif programlama veya veri akışı programlama gibi kavramlarla fonksiyonel programlamada iyi desteklenir. Dışarı deneyebilirsiniz reaktif sizin yan etkileri içerebilir olmadığını görmek için, Scala için CTP kütüphane.

Ayrıca, Haskell'den bir sayfa çıkarın: yan etkileri kapsüllemek / izole etmek için monad kullanın. Eyalet ve IO monad'larına bakın.


Cevabınız için Tyvm. Reaktif'ten klavye / fare girişi ve grafik / ses çıkışını nasıl alacağımdan emin değilim. Orada mı ve sadece özlüyorum? Bir monad kullanmaya ilişkin referansınıza gelince - şimdi onlar hakkında bir şeyler öğreniyorum ve hala bir monadın ne olduğunu tam olarak anlamıyorum.
chaotic3quilibrium

3

Deterministik olmayan parçalar (benim için) kullanıcı girişi akışını yönetiyor ... çıkışı (hem grafik hem de ses) yönetiyor.

Evet, IO deterministik değildir ve "her şey hakkında" yan etkilerdir. Bu Scala gibi saf olmayan işlevsel bir dilde bir sorun değildir.

uzaylı kurşun atışlarını belirlemek için rastgele bir değer getirme

Sahte bir sayı üretecinin çıkışını sonsuz bir dizi olarak ( SeqScala'da) ele alabilirsiniz.

...

Özellikle değişebilirliğe olan ihtiyacı nerede görüyorsunuz? Tahmin edersem, spritelarınızı uzayda zaman içinde değişen bir pozisyona sahip olarak düşünebilirsiniz. Böyle bir bağlamda "fermuarlar" hakkında düşünmek yararlı olabilir: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php


İdiyomatik işlevsel programlama olacak şekilde ilk kodu nasıl yapılandıracağımı bile bilmiyorum. Bundan sonra, "saf olmayan" kod eklemek için doğru (veya tercih edilen) tekniği anlamıyorum. Scala'yı "noktalı virgül içermeyen Java" olarak kullanabileceğimin farkındayım. Bunu yapmak istemiyorum. FP'nin zaman veya değer değişebilirliği sızıntılarına dayanmadan çok basit bir dinamik ortama nasıl hitap ettiğini öğrenmek istiyorum. Bu mantıklı mı?
chaotic3quilibrium
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.