[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. Je l'ai écrit dans la section commentaires de l'article original, mais comme c'est un gros problème, je vais organiser le processus de conception, etc. et en faire un article. Sujet: java j'ai essayé de casser un simple bloc --Qiita

image.png

conception

Je ne pense pas que l'extraction de classe soit si difficile parce que les programmes de jeu peuvent classer les apparences.

Le diagramme de classe est illustré ci-dessous. J'ai essayé de coder en couleur l'arrière-plan selon la figure célèbre pour son architecture épurée. image.png

UI, Web, DB, périphériques (partie bleue)

UI

Contrôleurs, présentateurs, passerelles (partie verte)

Contrôleurs (conversion d'entrée)

Je ne l'ai pas écrit sur le diagramme de classes parce que j'en ai fait une classe anonyme, mais comment l'écrire ...

Présentateurs (conversion de sortie)

Cas d'utilisation (partie rouge)

Le port de sortie de cas d'utilisation utilise les entités, mais est-ce une faute?

Entités (partie jaune)

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

Il existe également une interface de sortie de données qui peut remplacer les getters. (Interface rouge en jaune) Je pense que le vrai plaisir d'une architecture propre est de fournir une interface de sortie de données (Output Port) et d'inverser les dépendances.

De plus, j'ai essayé d'abstraire l'objet rebondi comme Bounder et l'objet rebondi (balle) comme Boundee afin qu'il puisse être traité indépendamment de la classe concrète.

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.

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("Rupture de bloc");
        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("Rupture de bloc", 300, 200);
        g.drawString("Il démarrera dans 5 secondes!", 250, 300);
        g.drawString("Le délai est de 30 secondes", 250, 400);
        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 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);
}

à la fin

J'ai écrit un contrôleur dans la classe interne à l'intérieur de la classe Breakout qui est l'interface utilisateur, mais je ne suis pas sûr que ce soit correct. 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
Avec ruby ● × Game et Othello (examen de base)
Dessinez des graphiques avec Sinatra et Chartkick
Préparer un environnement de scraping avec Docker et Java