본문 바로가기

끄적이기

[TIL - 8] 객체 설계 연습 : BrickBreaker 만들기 (JavaFX)

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를 사용하지 못했는데, 조금 더 고민해봐야겠다.