[JAVA] 자바 2D 게임 만들기 단상 (JFrame drawImage 속도 빠르게)

[JAVA] 자바 2D 게임 만들기 단상 (JFrame drawImage 속도 빠르게)

퓨어 자바로 게임을 짤 때마다 속도가 너무 느려서 좌절하곤 했었다.

원어로 된 관련 서적을 사서 읽어보기도 했지만, 핵심적인 부분을 찾아내지 못했고, 결국 포기했었다.

책에는 분명 퓨어 자바도 빠르게 그림을 찍어낼 수 있다고 쓰여있었는데 말이다.

각설하고, 이제 방법을 알아냈다.

알다시피 2D 게임을 만드는 가장 일반적인 방법은, 루프를 걸어놓고 그림 1장만 계속 찍어내는 것이다.

만약 800 x 600 해상도 게임을 만든다면, 가장 속도가 빠른 방법은 800 x 600 그림 1장을 반복적으로 찍어내는 것이다.

왜? 캐릭터, 타일 등을 곧바로 화면에 찍으면 느리다.

그러므로 800 x 600 짜리 이미지 객체를 만들어놓고, 타일이나 캐릭터 등을 그 이미지 객체에 찍는다.

그리고 완성된 이미지 객체를 화면에 찍는 것이다.

이렇게 해야 화면에 찍는 횟수를 최소화할 수 있다.

코드를 간략히 만들면, 아래와 같은 방식이다.

// 게임루프

while (true) {

    jFrame.getGraphics().drawImage(mainImg, 0, 0, null);

}

여기서 mainImg는 BufferedImage 객체다.

나는 여태까지 BufferedImage 객체이면 다 똑같은 객체인줄 알았다.

그런데 BufferedImage 객체는, 타입값에 따라 내부적으로 다르게 구성되는 특징이 있었고, 이게 키 포인트였다.

여태까지 나는 BufferedImage 의 픽셀을 수정하기 위해서 setRGB 메서드를 썼다.

가로 800, 세로 600 의 이중포문(for)을 돌면서 setRGB 메서드로 이미지 객체를 변경했는데,

이 setRGB 메서드가 엄청나게 느린 것이었다.

setRGB 메서드는 무한루프 안에서 사용하면 안되는 메서드였다.

하지만, 여태까지는 다른 대안을 몰라서 사용할 수 밖에 없었다.

for (int x=0; x<800; x++) {

    for (int y=0; y<600; y++) {

        // setRGB 메서드는 아주 느리다. 사용금지.

        mainImg.setRGB(x, y, rgbValue);

    }

}

그렇다면 다른 방법은 무엇일까?

우선 이미지를 만들 때 아래와 같이 만든다.

BufferedImage mainImg = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);

그러면 아래와 같이 int 배열을 뽑아낼 수가 있다.

int[] pixels = ((DataBufferInt) mainImg.getRaster().getDataBuffer()).getData();

RGB값을 아래와 같이 변경할 수 있다.

단순히 배열의 int 값을 바꾸면 된다.

// pixels.length == 480000 이다. 800 x 600 과 같다.

int len = pixels.length;

for (int i = 0; i < len; i++){
    pixels[i] = rgbValue;
}

이것이 중요한 이유는, setRGB 메서드보다 훨씬 빠른 속도로 RGB 값을 교체할 수 있기 때문이다.

(훨씬 빠르다는 표현이 부족할 정도로, 비교할 수 없을 정도로 큰 속도 차이가 난다. 이 코드 1줄로 인해 자바 2D 게임을 만들 수 있느냐 아니냐가 결정난다.)

마지막으로, int 값을 수정하기 쉽게 하려면, 게임 내에서 사용해야 하는 이미지들을 미리 읽어와야 한다.

게임루프에 들어가기 전 그림을 읽어와야 하는데, 이 때 BufferedImage.TYPE_INT_RGB 로 이미지를 읽어와야 한다.

그림파일을 BufferedImage 객체로 읽어오는 코드는 원래는 다음과 같이 쓴다.

public static BufferedImage loadImage(String filePath) throws Exception {

    BufferedImage image = null;

    File imgFile = new File(filePath);
    if (!imgFile.exists()) {
        return null;
    }

    image = ImageIO.read(imgFile);
    return image;
}

이 코드의 문제는, BufferedImage 객체를 읽어온 후 ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); 코드를 적용하면 오류가 발생한다는 점이다.

클래스 캐스트 익셉션이 발생한다. (java.lang.ClassCastException: java.awt.image.DataBufferByte cannot be cast to java.awt.image.DataBufferInt)

그러므로 미리 그림을 읽어오는 시점에 TYPE_INT_RGB 타입으로 저장하도록 아래처럼 코딩을 하자.

public static BufferedImage loadImageToRGB(String filePath) throws Exception {

    BufferedImage image = null;

    File imgFile = new File(filePath);
    if (!imgFile.exists()) {
        return null;
    }

    image = ImageIO.read(imgFile);
  
    int width = image.getWidth();
    int height = image.getHeight();
    BufferedImage rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    for (int x=0; x<width; x++) {
        for (int y=0; y<height; y++) {
            rgbImage.setRGB(x, y, image.getRGB(x, y));
        }
    }
  
     return rgbImage;
}

이렇게 모든 그림파일들을 TYPE_INT_RGB 타입의 BufferedImage 객체로 만들어놓고, 화면에 찍히는 메인 이미지의 int 배열의 값을 교체하는 방식이라면 간단한 2D 게임을 돌리는 데에는 충분한 속도가 나올 것이다.

전체 코드는 다음 포스트를 참고하자.

https://blog.naver.com/bb_/221585715646