[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 einem Bewusstsein für saubere Architektur neu zu gestalten und umzusetzen. Ich habe es im Kommentarbereich des Originalartikels geschrieben, aber da es eine große Sache ist, werde ich den Designprozess usw. organisieren und daraus einen Artikel machen. Betreff: Java Ich habe versucht, einen einfachen Block zu brechen - Qiita

image.png

Design

Ich denke nicht, dass Klassenextraktion so schwierig ist, weil Spielprogramme Auftritte klassifizieren können.

Das Klassendiagramm ist unten dargestellt. Ich habe versucht, den Hintergrund entsprechend der für seine saubere Architektur bekannten Figur farblich zu kennzeichnen. image.png

Benutzeroberfläche, Web, DB, Geräte (blauer Teil)

UI

Controller, Moderatoren, Gateways (grüner Teil)

Controller (Eingangsumwandlung)

Ich habe es nicht in das Klassendiagramm geschrieben, weil ich es zu einer anonymen Klasse gemacht habe, aber wie soll ich es schreiben ...

Moderatoren (Ausgabekonvertierung)

Anwendungsfälle (roter Teil)

Use Case Case Port verwendet Entities, aber ist es ein Foul?

Entitäten (gelber Teil)

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

Es gibt auch eine Datenausgabeschnittstelle, die Getter ersetzen kann. (Rote Schnittstelle in gelb) Ich denke, das wahre Vergnügen einer sauberen Architektur besteht darin, eine Datenausgabeschnittstelle (Output Port) bereitzustellen und die Abhängigkeiten umzukehren.

Außerdem habe ich versucht, das zurückgeworfene Objekt als Bounder und das zurückgeworfene Objekt (Ball) als Boundee zu abstrahieren, damit es unabhängig von der konkreten Klasse verarbeitet werden kann.

Implementierung

Der Begrüßungsschirm, die Blockanordnung und die Anordnung mit mehreren Kugeln sind auf das Originalwerk abgestimmt. Meine Richtlinie lautet, 25 Zeilen oder weniger pro Methode beizubehalten, damit Sie sie auf einem Bildschirm des Terminals sehen können.

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.awt.*;
import java.awt.event.*;
import java.awt.Color;
import java.awt.Rectangle;
import javax.swing.*;

public class Breakout extends JFrame {

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

    private final Splash splash = new Splash();
    private final BreakoutGame game = new BreakoutGame();
    private final BreakoutGameView view = new BreakoutGameView(game);
    private final CardLayout card = new CardLayout(0, 0);
    private Timer timer = null;

    public Breakout() {
        setTitle("Blockbrechen");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container pane = getContentPane();
        pane.setLayout(card);
        pane.add(splash, "splash");
        pane.add(view, "play");
        setupControllers();
        pack();
    }

    private void setupControllers() {
        view.addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseMoved(MouseEvent event) {
                game.moveRacket(event.getX());
                view.repaint();
            }
        });
    }

    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() {
        if (timer != null) timer.stop();
        card.show(getContentPane(), "play");
        timer = new javax.swing.Timer(50, (e) -> {
            game.update();
            view.repaint();
            if (game.isClear() || game.isOver()) {
                timer.stop();
                timer = null;
            }
        });
        timer.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("Blockbrechen", 300, 200);
        g.drawString("Es beginnt in 5 Sekunden!", 250, 300);
        g.drawString("Das Zeitlimit beträgt 30 Sekunden", 250, 400);
        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 BreakoutGameView extends JPanel {
    private final BreakoutGame game;

    public BreakoutGameView(BreakoutGame game) {
        this.game = game;
        setPreferredSize(new Dimension(game.WIDTH, game.HEIGHT));
        setBackground(Color.BLACK);
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        paintBalls(g);
        paintWalls(g);
        paintBlocks(g);
        paintRacket(g);
        if (game.isClear()) {
            paintGameClear(g);
        } else if (game.isOver()) {
            paintGameOver(g);
        }
    }

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

    private void paintWalls(Graphics g) {
        final int[] offset = {0, -16, -8};
        final int blockWidth = 26, blockHeight = 10, gapX = 6, gapY = 5;
        g.setColor(Color.GREEN);
        game.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);
                }
            }
        });
    }

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

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

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

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

class BreakoutGame {
    public static final int WIDTH = 855;
    public static final int HEIGHT = 800;
    public static final int WALL = 40;
    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 BreakoutGame() {
        court = new Court(WIDTH, HEIGHT, WALL);
        racket = new Racket(WIDTH / 2, HEIGHT - 110, 120, 5, WALL, WIDTH - WALL);
        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);
    }

    public boolean isClear() {
        return blocks.stream().allMatch(block -> block.isClear());
    }

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

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

    public void update() {
        for (var ball: balls) {
            ball.move(HEIGHT);
            court.rebound(ball);
            blocks.forEach(ball::bound);
            ball.bound(racket);  // ball bound racket after bounding blocks
        }
    }

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

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

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

    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 wall) {
        up = new Wall(0, 0, width, wall);
        left = new Wall(0, 0, wall, height);
        right = new Wall(width - wall, 0, wall, 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 isClear() {
        return strength <= 0;
    }

    public void view(BlockViewer viewer) {
        if (!isBroken()) {
            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 einen Controller in der inneren Klasse innerhalb der Breakout-Klasse geschrieben, die die Benutzeroberfläche ist, bin mir aber nicht sicher, ob dies in Ordnung ist. 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
Mit Rubin ● × Game und Othello (Grundlegende Bewertung)
Zeichnen Sie Diagramme mit Sinatra und Chartkick
Bereiten Sie eine Scraping-Umgebung mit Docker und Java vor