JavaFX 를 활용하여 간단한 BrickBreaker 를 만들어보았다.
간단한 게임(?)을 만들어보며, 구현 보다는 설계와 객체들 간의 상호작용을 고려하는데 집중했다. 금방 할 줄 알았는데, 생각보다 오래 걸려서 놀랐다.
그렇지만, 이제 막 시작하는 단계인지라 얻은 점도 많고 어떤 식으로 설계하면 좋을지 감은 잡은 것 같다. 나중에 이 글을 다시 봤을 때, 얼마나 고칠점이 많을지 다시 확인해보겠다.
우선 필요한 클래스부터 나열해보면, "공, 벽돌, 패들, 벽" 등이 있다.
- Ball : 게임 내 움직이는 공
- Brick : 깨야하는 벽돌
- Paddle : 좌우로 움직이며 공을 받아치는 패들
- Wall : 게임 화면 밖으로 벗어나지 않게 해주는 외벽
그리고 이를 연습삼아, 인터페이스와 추상 클래스 상속을 위해 Shape와 각각의 필요한 인터페이스들을 추가했다. 클래스 구조는 아래와 같다.

- Shape : 도형을 정의 (추상클래스)
- Rectangle, Circle : 사각형, 원을 표현
- Movable : 움직이는 객체들을 구분 (인터페이스)
- Drawable : 그릴 수 있는 (Canvas 에 draw 가능한) 객체들
- Bounceable : 공의 성질을 나타내기 위함
- 하위 클래스들은 위와 동일
Shape 클래스
public abstract class Shape {
protected double x, y; // 중심 좌표
public Shape(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public abstract double getMinX();
public abstract double getMaxX();
public abstract double getMinY();
public abstract double getMaxY();
}
그려야 하는 중심 좌표를 기준으로 모든 도형이 공통적으로 가지는 성질과 메서드만 선언했다.
Rectangle 클래스
// 사각형 클래스 -> 그릴 수 있는 형태
public class Rectangle extends Shape implements Drawable {
protected double width; // 가로 길이
protected double height; // 세로 길이
protected Color color; // 색상
public Rectangle(double x, double y, double width, double height, Color color) {
super(x, y);
this.width = width;
this.height = height;
this.color = color;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
public Color getColor() {
return color;
}
@Override
public void draw(GraphicsContext gc) {
gc.setFill(color);
gc.fillRect(getMinX(), getMinY(), width, height);
}
@Override
public double getMinX() {
return x - width / 2;
}
@Override
public double getMaxX() {
return x + width / 2;
}
@Override
public double getMinY() {
return y - height / 2;
}
@Override
public double getMaxY() {
return y + height / 2;
}
}
Rectangle 부터는 그릴 수 있는 객체라 판단. 아래의 Drawable 인터페이스를 구현했다.
public interface Drawable {
void draw(GraphicsContext gc);
}
Circle 클래스
// 원형 클래스 -> 그릴 수 있는 형태
public class Circle extends Shape implements Drawable {
protected double radius; // 반지름
protected Color color; // 색상
public Circle(double x, double y, double radius, Color color) {
super(x, y);
this.radius = radius;
this.color = color;
}
public double getRadius() {
return radius;
}
public Color getColor() {
return color;
}
@Override
public void draw(GraphicsContext gc) {
gc.setFill(color);
gc.fillOval(getMinX(), getMinY(), radius * 2, radius * 2); // 중심을 기준으로 원 그리기
}
@Override
public double getMinX() {
return x - radius;
}
@Override
public double getMaxX() {
return x + radius;
}
@Override
public double getMinY() {
return y - radius;
}
@Override
public double getMaxY() {
return y + radius;
}
}
Circle 클래스도 동일하게 Drawable을 구현했고, Rectangle 과는 다르게 반지름을 멤버로 가진다.
Ball 클래스
public class Ball extends Circle implements Movable, Bounceable{
private double dx; // 공의 x축 속도 (단위: 픽셀/프레임)
private double dy; // 공의 y축 속도 (단위: 픽셀/프레임)
private boolean isPaused; // 이동 가능 or 불가능
// 생성자
public Ball(double x, double y, double radius, double dx, double dy, Color color) {
super(x, y, radius, color);
this.dx = dx;
this.dy = dy;
this.isPaused = false;
}
@Override
public void move() {
if(!isPaused) {
x += dx;
y += dy;
}
}
@Override
public double getDx() {
return dx;
}
@Override
public double getDy() {
return dy;
}
@Override
public void setDx(double dx) {
this.dx = dx;
}
@Override
public void setDy(double dy) {
this.dy = dy;
}
@Override
public void pause() {
isPaused = true;
}
@Override
public void resume() {
isPaused = false;
}
// 충돌 감지 메서드
@Override
public boolean isCollisionDetected(Shape other) {
return getMaxX() >= other.getMinX() &&
getMinX() <= other.getMaxX() &&
getMaxY() >= other.getMinY() &&
getMinY() <= other.getMaxY();
}
// TODO - Step11 : Bounceable 성질 표현
@Override
public void bounce(Shape other) {
boolean collisionX = (getMaxX() >= other.getMinX() && getMinX() <= other.getMaxX());
boolean collisionY = (getMinY() <= other.getMaxY() && getMaxY() >= other.getMinY());
boolean collisionXY = (getMaxX() == other.getMinX() && getMaxY() == other.getMinY()) || // 좌상단 모서리
(getMaxX() == other.getMinX() && getMinY() == other.getMaxY()) || // 좌하단 모서리
(getMinX() == other.getMaxX() && getMaxY() == other.getMinY()) || // 우상단 모서리
(getMinX() == other.getMaxX() && getMinY() == other.getMaxY()); // 우하단 모서리
// 벽과의 충돌 계산은 조금 다름. (Wall 클래스 설계 참고)
if(other instanceof Wall) {
boolean collisionWithWallX = (getMinX() <= other.getMinX() || getMaxX() >= other.getMaxX());
boolean collisionWithWallY = (getMinY() <= other.getMinY() || getMaxY() >= other.getMaxY());
if(collisionWithWallX) {
setDx(-dx);
}
if(collisionWithWallY) {
setDy(-dy);
}
}
else {
// 모서리에 부딪혔을 경우
if(collisionXY) {
setDx(-dx);
setDy(-dy);
}
// 윗쪽, 아랫쪽과 부딪혔을 경우
else if(collisionY) {
setDy(-dy);
}
// 옆면과 부딪혔을 경우
else if(collisionX) {
setDx(-dx);
}
}
}
}
공의 움직임과, 다른 객체들과의 충돌을 판단하기 위해 메서드를 추가했다. 사실 Movable 인터페이스는 Paddle 도 있기 때문에 괜찮지만, Bounceable 인터페이스는 굳이 필요 없었을 것 같기도 하다. (튕기는게 공 밖에 없음)
큰 틀은 공의 상태를 변화해주는 메서드들과, 충돌 감지 메서드 두개다.
public interface Movable {
void move();
double getDx();
double getDy();
void setDx(double dx);
void setDy(double dy);
void pause();
void resume();
boolean isCollisionDetected(Shape other);
}
Brick 클래스
public class Brick extends Rectangle {
// 생성자
public Brick(double x, double y, double width, double height, Color color) {
super(x, y, width, height, color);
}
public void reflectBall(Ball ball) {
if(ball.isCollisionDetected(this)) {
ball.bounce(this);
}
}
}
공과의 충돌을 감지하고, 했다면 bounce 메서드를 호출 해 공의 상태를 변화시킨다.
Wall 클래스
public class Wall extends Rectangle {
public Wall(double x, double y, double width, double height, Color color) {
super(x, y, width, height, color);
}
// TODO - Step07 : 공이 땅에 닿았는지 확인용 메서드
public boolean downCollision(Ball ball) {
return ball.getMaxY() >= getMaxY();
}
// 공이 벽에 닿으면 튕겨내는 메서드
public void reflectBall(Ball ball) {
if(ball.isCollisionDetected(this)) {
ball.bounce(this);
}
}
@Override
public void draw(GraphicsContext gc) {
gc.setStroke(color);
gc.setLineWidth(3);
gc.strokeRect(getMinX(), getMinY(), width, height);
}
}
게임 화면을 벗어나지 않기 위한 Wall 클래스다. 다른 객체들과 다르게 strokeRect 를 사용해야 해서 선언된 draw 함수를 오버라이딩 했다. 마찬가지로 공이 벽에 닿으면 bounce를 호출하고, 바닥에 닿았다면 true값을 반환한다.
Paddle 클래스
public class Paddle extends Rectangle implements Movable {
private double speed; // 패들의 이동 속도
private boolean isPaused; // 이동 가능 or 불가능
// 생성자
public Paddle(double x, double y, double width, double height, double speed, Color color) {
super(x, y, width, height, color);
this.speed = speed;
this.isPaused = false;
}
// 패들의 위치를 왼쪽으로 이동
public void moveLeft() {
x -= speed;
}
// 패들의 위치를 오른쪽으로 이동
public void moveRight() {
x += speed;
}
// 패들이 Wall 안쪽을 벗어나지 않도록 설정
public void checkBounds(Wall wall) {
if (getMinX() < wall.getMinX()) { // 왼쪽 경계
x = wall.getMinX() + width / 2 ;
} else if (getMaxX() > wall.getMaxX()) { // 오른쪽 경계
x = wall.getMaxX() - width / 2;
}
}
// 공이 패들에 닿으면 튕겨내는 메서드
public void reflectBall(Ball ball) {
if(ball.isCollisionDetected(this)) {
ball.bounce(this);
}
}
@Override
public void move() {
if(!isPaused) {
x += speed;
}
}
@Override
public double getDx() {
return speed;
}
@Override
public double getDy() {
return 0;
}
@Override
public void setDx(double dx) {
speed = dx;
}
@Override
public void setDy(double dy) {}
@Override
public void pause() {
isPaused = true;
}
@Override
public void resume() {
isPaused = false;
}
@Override
public boolean isCollisionDetected(Shape other) {
return getMaxX() >= other.getMinX() &&
getMinX() <= other.getMaxX() &&
getMaxY() >= other.getMinY() &&
getMinY() <= other.getMaxY();
}
}
여기서 사실 move를 사용하지 못했는데, 조금 더 고민해봐야겠다.
'끄적이기' 카테고리의 다른 글
| [TIL - 10] 프로세스(Process) 정리 (0) | 2025.03.18 |
|---|---|
| [TIL - 9] Comparable, Comparator 완전 이해 (0) | 2025.03.12 |
| [TIL - 7] 객체 지향 설계를 어떻게 해야하나 (1) | 2025.03.06 |
| [TIL - 6] 웹 통신의 큰 흐름 (0) | 2025.03.05 |
| [TIL - 5] Comparable vs Comparator (0) | 2025.03.04 |