[JAVA] Concevoir et implémenter un jeu de rupture de bloc avec une architecture propre

introduction

J'étudie l'architecture propre et j'essaye diverses conceptions et implémentations. Il y a un article qui semble bon dans le sujet, et j'ai essayé de le repenser et de le réimplémenter avec une conscience d'architecture propre. J'ai commenté la proposition de conception et la proposition de mise en œuvre dans la section des commentaires de l'article original, mais comme elle a été affinée, j'organiserai le processus de conception, etc. Sujet: java j'ai essayé de casser un simple bloc --Qiita

image.png

conception

Une architecture propre

Dans l'architecture propre, les couches sont séparées comme indiqué dans la figure ci-dessous. clean.jpg

L'architecture propre montre également un diagramme de classes. clean_boundary.JPG

Jeu de rupture de bloc

Je ne pense pas que ce soit si difficile d'extraire la classe du programme de jeu car vous pouvez en faire une classe.

J'ai divisé les classes en gardant à l'esprit les couches.

Cadres et pilotes (partie bleue)

UI

Devices

JFrame et JPanel combinent un périphérique de souris, un périphérique de clavier et un périphérique d'affichage.

Web

Je pense qu'il est possible de permettre de jouer à des jeux avec un navigateur Web, mais je vais l'omettre.

DB

Il est possible d'enregistrer le meilleur score du jeu, mais de l'omettre.

External Interfaces

Il est possible de se connecter à un utilisateur distant via un réseau et de jouer contre lui, mais l'omettre.

Adaptateurs d'interface (partie verte)

Un groupe d'adaptateurs de conversion de données pour joindre (adapter) des cadres et des pilotes (UI, Web, DB, périphériques, etc.) dans le monde extérieur et des cas d'utilisation dans le monde intérieur.

Contrôleurs (conversion d'entrée)

Présentateurs (conversion de sortie)

Gateways

La conversion de sérialisation du relais de données de combat peut être envisagée, mais elle sera omise.

Règles métier d'application (partie rouge)

Use Cases

Règles d'entreprise d'entreprise (partie jaune)

Entities

Un groupe de classes contenant des données telles que des informations de position, des informations de taille et des informations de couleur.

Le diagramme de classe du jeu de rupture de bloc est illustré ci-dessous. J'ai essayé de coder en couleur l'arrière-plan selon le diagramme de couches de l'architecture propre. image.png Pour Entity, j'ai créé une interface de sortie de données pour remplacer le getter. (Interface rouge en jaune) En outre, l'objet rebondi est abstrait en tant que Bounder, et l'objet rebondi (balle) est abstrait en tant que Boundee afin qu'il puisse être traité indépendamment de la classe concrète.

La partie inférieure droite du diagramme de couches de Clean Architecture est illustrée dans la figure ci-dessous. image.png

Lorsqu'il est appliqué au diagramme de configuration de classe de l'architecture propre, il est comme indiqué dans la figure ci-dessous. La partie Données d'entrée n'est pas due à l'utilisation de types primitifs. image.png La partie Output Data est un stockage de données de «», et je pense qu'il est censé être utilisé pour créer et notifier des données à chaque fois. Dans la version initiale, BreakoutViewData correspondant à Output Data était défini comme une classe pour avoir des données, mais je pensais que c'était redondant, alors j'en ai fait une interface pour extraire les données de BreakoutUseCase. De plus, ce n'est pas seulement un getter, mais les données primitives sont transmises à l'interface de rappel qui transmet les données afin que les données ne puissent pas être modifiées.

la mise en oeuvre

L'écran de démarrage, la disposition des blocs et la disposition des balles multiples sont adaptés à l'œuvre originale. Ma politique est de conserver 25 lignes ou moins par méthode afin que vous puissiez le voir sur un écran du terminal.

Code source

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("Rupture de bloc");
        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("Rupture de bloc", 300, 200);
        g.drawString("Il démarrera dans 5 secondes!", 250, 300);
        g.drawString("Déplacez la souris sur le côté pour faire rebondir la balle", 125, 500);
        g.drawString("La pointe de la flèche ci-dessous correspond à la position initiale de la souris", 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);
}

à la fin

J'ai fait de BreakoutViewData une interface, mais je ne suis pas sûr de pouvoir le faire de cette façon. Si vous avez des conseils sur la façon de procéder, laissez un commentaire.

Recommended Posts

Concevoir et implémenter un jeu de rupture de bloc avec une architecture propre
Concevoir et implémenter un jeu de rupture de bloc avec une architecture propre
Un simple jeu de ciseaux-papier-pierre avec JavaFX et SceneBuilder
[Critique de livre] Structure et conception du logiciel d'architecture propre appris des maîtres
Considération sur les rails et l'architecture propre
Faites un jeu de frappe avec ruby
Dessinez des graphiques avec Sinatra et Chartkick
Préparer un environnement de scraping avec Docker et Java