Last active
August 31, 2020 23:45
-
-
Save shiracamus/12f74c5271e2ae406f17dfe83c7b6707 to your computer and use it in GitHub Desktop.
CleanArchitecture Breakout.java on QIita https://qiita.com/shiracamus/items/54b7bf1af018ce829335
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("ブロック崩し"); | |
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("ブロック崩し", 300, 200); | |
g.drawString("五秒後に始まるよー!", 250, 300); | |
g.drawString("マウスを横に動かしてボールを跳ね返そう", 125, 500); | |
g.drawString("マウスは下の矢印の先っぽがが初期位置でつ", 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 GameView view = new GameView(viewModel, BreakoutStage.WIDTH, BreakoutStage.HEIGHT); | |
private final BreakoutController controller = new BreakoutController(uc, view); | |
public BreakoutGame() { | |
presenter.addListener(model -> view.repaint()); | |
presenter.addListener(model -> { | |
if (viewModel.isGameClear() || viewModel.isGameOver()) { | |
stop(); | |
} | |
}); | |
} | |
public Component getView() { | |
return view; | |
} | |
public void start() { | |
controller.enable(); | |
} | |
public void stop() { | |
controller.disable(); | |
} | |
} | |
class GameView extends JPanel { | |
private final GameViewModel model; | |
public GameView(GameViewModel 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); | |
} | |
} | |
interface GameViewModel { | |
public void paint(Graphics g); | |
} | |
class BreakoutViewModel implements GameViewModel { | |
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(); | |
} | |
@Override | |
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 { | |
protected final BreakoutViewer viewer; | |
protected final BreakoutViewData viewData = createViewData(); | |
protected BreakoutStage stage = createStage(); | |
public BreakoutUseCase(BreakoutViewer viewer) { | |
this.viewer = viewer; | |
} | |
public BreakoutStage createStage() { | |
return new BreakoutStage1(); | |
} | |
public BreakoutViewData createViewData() { | |
return new BreakoutViewData() { | |
@Override | |
public boolean isGameClear() { | |
return stage.isClear(); | |
} | |
@Override | |
public boolean isGameOver() { | |
return stage.isOver(); | |
} | |
@Override | |
public void viewWalls(WallViewer viewer) { | |
stage.viewWalls(viewer); | |
} | |
@Override | |
public void viewBlocks(BlockViewer viewer) { | |
stage.viewBlocks(viewer); | |
} | |
@Override | |
public void viewRacket(RacketViewer viewer) { | |
stage.viewRacket(viewer); | |
} | |
@Override | |
public void viewBalls(BallViewer viewer) { | |
stage.viewBalls(viewer); | |
} | |
}; | |
} | |
@Override | |
public void moveRacket(int x) { | |
stage.moveRacket(x); | |
output(); | |
} | |
@Override | |
public void moveBalls() { | |
stage.moveBalls(); | |
output(); | |
} | |
public void output() { | |
viewer.view(viewData); | |
} | |
} | |
abstract class BreakoutStage { | |
public static final int WIDTH = 855; | |
public static final int HEIGHT = 800; | |
public static final int WALL_SIZE = 40; | |
protected final Random rand = new Random(); | |
protected final Court court = makeCourt(); | |
protected final Racket racket = makeRacket(); | |
protected final List<Block> blocks = makeBlocks(); | |
protected final List<Ball> balls = makeBalls(); | |
protected Court makeCourt() { | |
return new Court(WIDTH, HEIGHT, WALL_SIZE); | |
} | |
abstract protected Racket makeRacket(); | |
abstract protected List<Block> makeBlocks(); | |
abstract protected List<Ball> makeBalls(); | |
public void moveRacket(int x) { | |
racket.move(x); | |
} | |
public void moveBalls() { | |
balls.forEach(ball -> { | |
ball.move(HEIGHT); | |
court.rebound(ball); | |
blocks.forEach(ball::bound); | |
ball.bound(racket); | |
}); | |
} | |
public boolean isClear() { | |
return blocks.stream().allMatch(block -> block.isCleared()); | |
} | |
public boolean isOver() { | |
return balls.stream().allMatch(ball -> ball.isDead()); | |
} | |
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 BreakoutStage1 extends BreakoutStage { | |
@Override | |
protected Racket makeRacket() { | |
return new Racket(WIDTH / 2, HEIGHT - 110, 120, 5, WALL_SIZE, WIDTH - WALL_SIZE); | |
} | |
@Override | |
protected 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; | |
} | |
protected 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)); | |
} | |
} | |
} | |
@Override | |
protected List<Ball> makeBalls() { | |
return Arrays.asList(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), | |
}); | |
} | |
protected Ball makeBall(int y, int vx, int vy, int size) { | |
return new Ball(40 + rand.nextInt(700), y, vx, vy, size); | |
} | |
} | |
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 Racket extends Bounder { | |
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 { | |
} | |
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); | |
} | |
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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment