[JAVA] Entwerfen und implementieren Sie ein Block Breaking-Spiel mit einer sauberen Architektur

Einführung

Ich studiere saubere Architektur und probiere verschiedene Designs und Implementierungen aus. Es gab einen Artikel, der für das Thema gut zu sein schien, und ich versuchte, ihn mit dem Bewusstsein für saubere Architektur neu zu gestalten und umzusetzen. Ich habe den Entwurfsvorschlag und den Implementierungsvorschlag im Kommentarbereich des Originalartikels kommentiert, aber da er weiter verfeinert wurde, werde ich den Entwurfsprozess usw. organisieren und daraus einen Artikel machen. Betreff: Java Ich habe versucht, einen einfachen Block zu brechen - Qiita

image.png

Design

Saubere Architektur

In der sauberen Architektur werden die Ebenen wie in der folgenden Abbildung gezeigt getrennt. clean.jpg

Die saubere Architektur zeigt auch ein Klassendiagramm. clean_boundary.JPG

Block Breaking Spiel

Ich denke nicht, dass es so schwierig ist, die Klasse des Spielprogramms zu extrahieren, weil das Erscheinungsbild zu einer Klasse gemacht werden kann.

Ich habe die Klassen aufgeteilt und dabei die Ebenen berücksichtigt.

Frameworks & Treiber (blauer Teil)

UI

Devices

JFrame und JPanel kombinieren ein Mausgerät, ein Tastaturgerät und ein Anzeigegerät.

Web

Ich denke, es ist möglich, Spiele in einem Webbrowser verfügbar zu machen, aber ich werde es weglassen.

DB

Es ist möglich, den Highscore des Spiels aufzuzeichnen, aber wegzulassen.

External Interfaces

Es ist möglich, über ein Netzwerk eine Verbindung zu einem Remote-Benutzer herzustellen und gegeneinander zu spielen, aber lassen Sie es weg.

Schnittstellenadapter (grüner Teil)

Eine Gruppe von Datenkonvertierungsadaptern zum Verbinden (Anpassen) von Frameworks und Treibern (Benutzeroberfläche, Web, DB, Geräte usw.) in der Außenwelt und Anwendungsfällen in der Innenwelt.

Controller (Eingangsumwandlung)

Moderatoren (Ausgabekonvertierung)

Gateways

Die Serialisierungskonvertierung von Battle Data Relay kann in Betracht gezogen werden, wird jedoch weggelassen.

Anwendungsgeschäftsregeln (roter Teil)

Use Cases

Unternehmensgeschäftsregeln (gelber Teil)

Entities

Eine Gruppe von Klassen mit Daten wie Positionsinformationen, Größeninformationen und Farbinformationen.

Das Klassendiagramm des Blockbrechspiels ist unten dargestellt. Ich habe versucht, den Hintergrund gemäß dem Layer-Diagramm der sauberen Architektur farblich zu kennzeichnen. image.png Für Entity habe ich eine Datenausgabeschnittstelle erstellt, um den Getter zu ersetzen. (Rote Schnittstelle in gelb) Darüber hinaus wird das zurückgeworfene Objekt als Bounder abstrahiert, und das zurückgeworfene Objekt (Ball) wird als Boundee abstrahiert, so dass es unabhängig von der konkreten Klasse verarbeitet werden kann.

Der untere rechte Teil des Layer-Diagramms von Clean Architecture ist in der folgenden Abbildung dargestellt. image.png

Wenn es auf das Klassenkonfigurationsdiagramm der sauberen Architektur angewendet wird, ist es wie in der folgenden Abbildung gezeigt. Der Teil Eingabedaten ist nicht auf die Verwendung primitiver Typen zurückzuführen. image.png Der Ausgabedatenteil ist ein Datenspeicher von "", und ich denke, dass er jedes Mal zum Erstellen und Benachrichtigen von Daten verwendet werden soll. In der ersten Version wurde BreakoutViewData, das Ausgabedaten entspricht, als Klasse für Daten festgelegt, aber ich fand es redundant, sodass ich es zu einer Schnittstelle zum Abrufen der Daten von BreakoutUseCase gemacht habe. Darüber hinaus ist es nicht nur ein Getter, sondern es werden primitive Daten an die Rückrufschnittstelle übergeben, die die Daten weitergibt, sodass die Daten nicht geändert werden können.

Implementierung

Der Begrüßungsschirm, die Blockanordnung und die Anordnung mit mehreren Kugeln sind auf das Originalwerk abgestimmt. Es ist meine Richtlinie, 25 Zeilen oder weniger pro Methode beizubehalten, damit sie auf einem Bildschirm des Terminals angezeigt werden.

Quellcode

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.function.Consumer;
import java.awt.Component;
import java.awt.CardLayout;
import java.awt.Graphics;
import java.awt.Font;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Breakout extends JFrame {

    public static void main(String[] args) {
        var breakout = new Breakout();
        breakout.playAfterSplash(5);
        breakout.setVisible(true);
    }

    private final BreakoutGame game = new BreakoutGame();
    private final CardLayout card = new CardLayout(0, 0);
    private Timer timer;

    public Breakout() {
        setTitle("Blockbrechen");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        var pane = getContentPane();
        pane.setLayout(card);
        pane.add(new Splash(), "splash");
        pane.add(game.getView(), "game");
        pack();
    }

    public void playAfterSplash(int sec) {
        card.show(getContentPane(), "splash");
        timer = new javax.swing.Timer(sec * 1000, e -> {
            timer.stop();
            timer = null;
            play();
        });
        timer.start();
    }

    public void play() {
        card.show(getContentPane(), "game");
        game.start();
    }
}

class Splash extends JPanel {
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.setFont(new Font("TimeRoman", Font.CENTER_BASELINE, 30));
        g.setColor(Color.red);
        g.drawString("Blockbruch", 300, 200);
        g.drawString("Es beginnt in 5 Sekunden!", 250, 300);
        g.drawString("Bewegen Sie die Maus zur Seite, um den Ball abzuprallen", 125, 500);
        g.drawString("Die Pfeilspitze unten ist die Anfangsposition der Maus", 100, 600);
        g.drawString("↓", 425, 700);
    }
}

class BreakoutGame {
    private final BreakoutViewModel viewModel = new BreakoutViewModel();
    private final BreakoutPresenter presenter = new BreakoutPresenter(viewModel);
    private final BreakoutUseCase uc = new BreakoutUseCase(presenter);
    private final BreakoutView view = new BreakoutView(viewModel, uc.WIDTH, uc.HEIGHT);
    private final BreakoutController controller = new BreakoutController(uc, view);

    public BreakoutGame() {
        presenter.addListener(viewModel -> view.repaint());
        presenter.addListener(viewModel -> {
            if (viewModel.isGameClear() || viewModel.isGameOver()) {
                stop();
            }
        });
    }

    public Component getView() {
        return view;
    }

    public void start() {
        controller.enable();
    }

    public void stop() {
        controller.disable();
    }
}

class BreakoutView extends JPanel {
    private final BreakoutViewModel model;

    public BreakoutView(BreakoutViewModel model, int width, int height) {
        this.model = model;
        setPreferredSize(new Dimension(width, height));
        setBackground(Color.black);
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        model.paint(g);
    }
}

class BreakoutViewModel {
    private BreakoutViewData data;

    public void update(BreakoutViewData data) {
        this.data = data;
    }

    public boolean isAvailable() {
        return data != null;
    }

    public boolean isGameClear() {
        return data.isGameClear();
    }

    public boolean isGameOver() {
        return data.isGameOver();
    }

    public void paint(Graphics g) {
        if (!isAvailable()) return;
        paintBalls(g);
        paintWalls(g);
        paintBlocks(g);
        paintRacket(g);
        if (isGameClear()) {
            paintGameClear(g);
        } else if (isGameOver()) {
            paintGameOver(g);
        }
    }

    public void paintWalls(Graphics g) {
        final int[] offset = {0, -16, -8};
        final int blockWidth = 26, blockHeight = 10, gapX = 6, gapY = 5;
        g.setColor(Color.GREEN);
        data.viewWalls((wallX, wallY, wallWidth, wallHeight) -> {
            for (int y = wallY, iy = 0; y < wallY + wallHeight; y += blockHeight + gapY, iy++) {
                for (int blockX = wallX + offset[iy % offset.length]; blockX < wallX + wallWidth; blockX += blockWidth + gapX) {
                    int x = blockX;
                    int width = blockWidth;
                    int height = blockHeight;
                    if (x < wallX) {
                        x = wallX;
                        width -= wallX - blockX;
                    } else if (x + blockWidth >= wallX + wallWidth) {
                        width = wallX + wallWidth - x;
                    }
                    if (wallY + height >= wallY + wallHeight) {
                        height = wallY + wallHeight - y;
                    }
                    g.fillRect(x, y, width, height);
                }
            }
        });
    }

    public void paintBlocks(Graphics g) {
        data.viewBlocks((x, y, width, height, color) -> {
            g.setColor(color);
            g.fillRect(x, y, width, height);
        });
    }

    public void paintRacket(Graphics g) {
        g.setColor(Color.WHITE);
        data.viewRacket((x, y, width, height) -> g.fillRect(x, y, width, height));
    }

    private void paintBalls(Graphics g) {
        g.setColor(Color.RED);
        data.viewBalls((x, y, size) -> g.fillOval(x, y, size, size));
    }

    public void paintGameClear(Graphics g) {
        g.setFont(new Font("TimeRoman", Font.BOLD, 50));
        g.setColor(Color.orange);
        g.drawString("Game Clear!", 300, 550);
    }

    public void paintGameOver(Graphics g) {
        g.setFont(new Font("TimeRoman", Font.BOLD, 50));
        g.setColor(Color.orange);
        g.drawString("Game Over!", 300, 550);
    }
}

class BreakoutPresenter implements BreakoutViewer {
    private final BreakoutViewModel viewModel;
    private final List<Consumer<BreakoutViewModel>> listeners = new ArrayList<>();

    public BreakoutPresenter(BreakoutViewModel viewModel) {
        this.viewModel = viewModel;
    }

    public void addListener(Consumer<BreakoutViewModel> listener) {
        listeners.add(listener);
    }

    public void removeListener(Consumer<BreakoutViewModel> listener) {
        listeners.remove(listener);
    }

    @Override
    public void view(BreakoutViewData data) {
        viewModel.update(data);
        listeners.forEach(listener -> listener.accept(viewModel));
    }
}

class BreakoutController {
    public static final int MOVE_BALLS_INTERVAL_MILLISEC = 50;
    private final BreakoutOperation operation;
    private final Component mouseDevice;
    private final MouseMotionListener mouseController;
    private final Timer ballController;

    public BreakoutController(BreakoutOperation operation, Component mouseDevice) {
        this.operation = operation;
        this.mouseDevice = mouseDevice;
        mouseController = makeMouseController();
        ballController = makeBallController(MOVE_BALLS_INTERVAL_MILLISEC);
    }

    private MouseMotionListener makeMouseController() {
        return new MouseMotionAdapter() {
            @Override
            public void mouseMoved(MouseEvent event) {
                operation.moveRacket(event.getX());
            }
        };
    }

    private Timer makeBallController(int interval_millisec) {
        return new javax.swing.Timer(interval_millisec, e -> {
            operation.moveBalls();
        });
    }

    public void enable() {
        mouseDevice.addMouseMotionListener(mouseController);
        ballController.start();
    }

    public void disable() {
        mouseDevice.removeMouseMotionListener(mouseController);
        ballController.stop();
    }
}

interface BreakoutOperation {
    public void moveRacket(int x);
    public void moveBalls();
}

interface BreakoutViewer {
    public void view(BreakoutViewData data);
}

interface BreakoutViewData {
    public boolean isGameClear();
    public boolean isGameOver();
    public void viewWalls(WallViewer viewer);
    public void viewBlocks(BlockViewer viewer);
    public void viewRacket(RacketViewer viewer);
    public void viewBalls(BallViewer viewer);
}

class BreakoutUseCase implements BreakoutOperation, BreakoutViewData {
    public static final int WIDTH = 855;
    public static final int HEIGHT = 800;
    public static final int WALL_SIZE = 40;
    private final BreakoutViewer viewer;
    private final Court court;
    private final Racket racket;
    private final List<Block> blocks;
    private final List<Ball> balls;
    private final Random rand = new Random();

    public BreakoutUseCase(BreakoutViewer viewer) {
        this.viewer = viewer;
        court = new Court(WIDTH, HEIGHT, WALL_SIZE);
        racket = new Racket(WIDTH / 2, HEIGHT - 110, 120, 5, WALL_SIZE, WIDTH - WALL_SIZE);
        blocks = makeBlocks();
        balls = Arrays.asList(makeBalls());
    }

    private List<Block> makeBlocks() {
        List<Block> blocks = new ArrayList<>();
        addBlocks(blocks,  60, 40, 16, 40, 48, 8, 4, Color.YELLOW, 3);
        addBlocks(blocks, 300, 40, 16, 40, 16, 8, 3, Color.GREEN, 2);
        addBlocks(blocks, 450, 40, 16, 40, 16, 8, 1, Color.GRAY, Block.UNBREAKABLE);
        addBlocks(blocks, 600, 40, 16, 40, 16, 9, 4, Color.CYAN, 1);
        return blocks;
    }

    private void addBlocks(List<Block> blocks, int topY,
                           int width, int height, int gapX, int gapY,
                           int cols, int rows, Color color, int strength) {
        int topX = (WIDTH - width * cols - gapX * (cols - 1)) / 2;
        int endY = topY + (height + gapY) * rows;
        int endX = topX + (width + gapX) * cols;
        for (int y = topY; y < endY; y += height + gapY) {
            for (int x = topX; x < endX; x += width + gapX) {
                blocks.add(new Block(x, y, width, height, color, strength));
            }
        }
    }

    private Ball[] makeBalls() {
        return new Ball[] {
            makeBall(250, 5, -6, 7),
            makeBall(260, -5, -3, 10),
            makeBall(420, 4, 6, 8),
            makeBall(480, -5, 2, 10),
            makeBall(590, 5, -6, 11),
            makeBall(550, -5, -3, 12),
            makeBall(570, 4, 6, 13),
            makeBall(480, -5, 2, 14),
            makeBall(490, 5, -6, 8),
            makeBall(400, -5, -3, 8),
            makeBall(350, 4, 6, 9),
            makeBall(400, -5, 2, 10),
            makeBall(390, -5, -3, 10),
            makeBall(500, 4, 6, 10),
            makeBall(530, -5, 2, 7),
        };
    }

    private Ball makeBall(int y, int vx, int vy, int size) {
        return new Ball(40 + rand.nextInt(700), y, vx, vy, size);
    }

    @Override
    public void moveRacket(int x) {
        racket.move(x);
        output();
    }

    @Override
    public void moveBalls() {
        balls.forEach(ball -> {
            ball.move(HEIGHT);
            court.rebound(ball);
            blocks.forEach(ball::bound);
            ball.bound(racket);
        });
        output();
    }

    private void output() {
        viewer.view(this);
    }

    @Override
    public boolean isGameClear() {
        return blocks.stream().allMatch(block -> block.isCleared());
    }

    @Override
    public boolean isGameOver() {
        return balls.stream().allMatch(ball -> ball.isDead());
    }

    @Override
    public void viewWalls(WallViewer viewer) {
        court.viewWalls(viewer);
    }

    @Override
    public void viewBlocks(BlockViewer viewer) {
        blocks.forEach(block -> block.view(viewer));
    }

    @Override
    public void viewRacket(RacketViewer viewer) {
        racket.view(viewer);
    }

    @Override
    public void viewBalls(BallViewer viewer) {
        balls.forEach(ball -> ball.view(viewer));
    }
}

class Bounder {
    protected Rectangle rect;

    public Bounder(int x, int y, int width, int height) {
        this.rect = new Rectangle(x, y, width, height);
    }

    public boolean isHit(int x, int y) {
        return this.rect.contains(x, y);
    }

    public void hit() {
        // default: nothing to do
    }

    public void view(BounderViewer viewer) {
        viewer.view(rect.x, rect.y, rect.width, rect.height);
    }
}

interface BounderViewer {
    public void view(int x, int y, int width, int height);
}

class Court {
    private final Wall up, left, right;

    public Court(int width, int height, int wallSize) {
        up = new Wall(0, 0, width, wallSize);
        left = new Wall(0, 0, wallSize, height);
        right = new Wall(width - wallSize, 0, wallSize, height);
    }

    public boolean isHit(int x, int y) {
        return up.isHit(x, y) || left.isHit(x, y) || right.isHit(x, y);
    }

    public void rebound(Boundee boundee) {
        boundee.bound(up);
        boundee.bound(left);
        boundee.bound(right);
    }

    public void viewWalls(WallViewer viewer) {
        up.view(viewer);
        left.view(viewer);
        right.view(viewer);
    }
}

class Wall extends Bounder {
    public Wall(int x, int y, int width, int height) {
        super(x, y, width, height);
    }
}

interface WallViewer extends BounderViewer {
}

class Block extends Bounder {
    public static final int UNBREAKABLE = -1;
    private static final int BROKEN = 0;
    private final Color color;
    private int strength;

    public Block(int x, int y, int width, int height, Color color, int strength) {
        super(x, y, width, height);
        this.color = color;
        this.strength = strength;
    }

    @Override
    public boolean isHit(int x, int y) {
        return isBroken() ? false : super.isHit(x, y);
    }

    @Override
    public void hit() {
        if (strength > 0) strength--;
    }

    public boolean isBroken() {
        return strength == BROKEN;
    }

    public boolean isCleared() {
        return strength <= 0;
    }

    public void view(BlockViewer viewer) {
        if (isBroken()) return;
        viewer.view(rect.x, rect.y, rect.width, rect.height, color);
    }
}

interface BlockViewer {
    public void view(int x, int y, int width, int height, Color color);
}

class Racket extends Bounder {
    public static final int SPEED = 5;
    private final int left, right;

    public Racket(int centerX, int centerY, int width, int height,
                  int limitLeft, int limitRight) {
        super(centerX - width / 2, centerY - height / 2, width, height);
        left = limitLeft;
        right = limitRight - width;
    }

    public void move(int x) {
        x -= rect.width / 2;
        rect.x = x < left ? left
               : x > right ? right
               : x;
    }
}

interface RacketViewer extends BounderViewer {
}

interface Boundee {
    public void bound(Bounder bounder);
}

class Ball implements Boundee {
    private int x, y, vx, vy;
    private final int size, r;
    private boolean alive = true;

    public Ball(int x, int y, int vx, int vy, int size) {
        this.x = x;
        this.y = y;
        this.vx = vx;
        this.vy = vy;
        this.size = size | 1;  // always odd
        this.r = this.size / 2;
    }

    public void move(int bottomY) {
        if (alive) {
            x += vx;
            y += vy;
            alive = y - r < bottomY;
        }
    }

    @Override
    public void bound(Bounder bounder) {
        boolean up = bounder.isHit(x, y - r);
        boolean down = bounder.isHit(x, y + r);
        boolean left = bounder.isHit(x - r, y);
        boolean right = bounder.isHit(x + r, y);
        boolean up_left = bounder.isHit(x - r, y - r);
        boolean up_right = bounder.isHit(x + r, y - r);
        boolean down_left = bounder.isHit(x - r, y + r);
        boolean down_right = bounder.isHit(x + r, y + r);
        if (vy < 0 && up && !bounder.isHit(x, y - r - vy) ||
            vy > 0 && down && !bounder.isHit(x, y + r - vy)) {
            bounder.hit();
            vy *= -1;
        } else if (vx < 0 && left && !bounder.isHit(x - r - vx, y - r) ||
                   vx > 0 && right && !bounder.isHit(x + r - vx, y - r)) {
            bounder.hit();
            vx *= -1;
        } else if (up_left && vx < 0 && vy < 0 ||
                   up_right && vx > 0 && vy < 0 ||
                   down_left && vx < 0 && vy > 0 ||
                   down_right && vx > 0 && vy > 0) {
            bounder.hit();
            vy *= -1;
            vx *= -1;
        }
    }

    public boolean isDead() {
        return !alive;
    }

    public void view(BallViewer viewer) {
        viewer.view(x - r, y - r, size);
    }
}

interface BallViewer {
    public void view(int x, int y, int size);
}

schließlich

Ich habe BreakoutViewData zu einer Schnittstelle gemacht, bin mir aber nicht sicher, ob ich das so machen kann. Wenn Sie dazu einen Rat haben, hinterlassen Sie bitte einen Kommentar.

Recommended Posts

Entwerfen und implementieren Sie ein Block Breaking-Spiel mit einer sauberen Architektur
Entwerfen und implementieren Sie ein Block Breaking-Spiel mit einer sauberen Architektur
Ein einfaches Stein-Papier-Scheren-Spiel mit JavaFX und SceneBuilder
[Buchbesprechung] Saubere Architektur Struktur und Design der Software von Meistern gelernt
Überlegungen zu Schienen und sauberer Architektur
Machen Sie ein Tippspiel mit Ruby
Zeichnen Sie Diagramme mit Sinatra und Chartkick
Bereiten Sie eine Scraping-Umgebung mit Docker und Java vor