728x90
저번 글 1: https://stickode.tistory.com/822
저번 글 2: https://stickode.tistory.com/835
저번 글 3: https://stickode.tistory.com/852
저번 글 4: https://stickode.tistory.com/866
저번 글 5: https://stickode.tistory.com/875
안녕하세요. 저번 시간에 이어서, 오늘은 SquareBoard 코드에 대한 설명을 업로드하겠습니다. 주석으로 설명을 달아두었습니다.
import javax.swing.*;
import java.awt.*;
import java.util.Hashtable;
/**
* 테트리스 스퀘어 보드입니다. 보드는 직사각형이며, 색상으로 구성된 격자를 포함합니다.
* 보드는 왼쪽과 오른쪽, 그리고 아래쪽에 제약이 있는 것으로 간주됩니다.
* 보드의 위쪽에는 제약이 없지만, 보드 위쪽의 위치에 할당된 색상은 저장되지 않습니다.
*/
public class SquareBoard extends Object {
/**
* 보드 너비 (스퀘어 수)
*/
private final int width;
/**
* 보드 높이 (스퀘어 수)
*/
private final int height;
/**
* 스퀘어 보드 색상 매트릭스입니다. 이 매트릭스(또는 그리드)에는
* 보드의 각 스퀘어에 대한 색상 항목이 포함되어 있습니다.
* 매트릭스는 수직 및 수평 좌표로 색인됩니다.
*/
private Color[][] matrix = null;
/**
* 선택적인 보드 메시지입니다. 보드 메시지는 언제든지 설정할 수 있으며,
* 이는 보드 위에 인쇄됩니다.
*/
private String message = null;
/**
* 제거된 라인 수입니다. 이 카운터는 보드에서 라인이 제거될 때마다 증가합니다.
*/
private int removedLines = 0;
/**
* 그래픽 스퀘어 보드 컴포넌트입니다. 이 그래픽 표현은
* getComponent()가 처음 호출될 때 생성됩니다.
*/
private final SquareBoardComponent component;
/**
* 지정된 크기로 새로운 스퀘어 보드를 생성합니다. 스퀘어 보드는
* 초기에 비어있을 것입니다.
*
* @param width 보드의 너비 (스퀘어 수)
* @param height 보드의 높이 (스퀘어 수)
*/
public SquareBoard(int width, int height) {
this.width = width;
this.height = height;
this.matrix = new Color[height][width];
this.component = new SquareBoardComponent();
clear();
}
/**
* 지정된 스퀘어가 비어있는지 확인합니다. 즉, 해당 스퀘어가 색상으로
* 표시되지 않은 경우입니다. 스퀘어가 보드의 바깥에 있는 경우,
* 스퀘어가 보드의 바로 위에 있는 경우를 제외하고 모든 경우에
* false가 반환됩니다.
*
* @param x 가로 위치 (0 <= x < width)
* @param y 세로 위치 (0 <= y < height)
* @return 스퀘어가 비어있는 경우 true, 그렇지 않으면 false
*/
public boolean isSquareEmpty(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return x >= 0 && x < width && y < 0;
} else {
return matrix[y][x] == null;
}
}
/**
* 지정된 라인이 비어있는지 확인합니다. 즉, 비어있는 스퀘어만
* 포함하는 경우입니다. 라인이 보드의 바깥에 있는 경우는 항상
* false가 반환됩니다.
*
* @param y 세로 위치 (0 <= y < height)
* @return 전체 라인이 비어있는 경우 true, 그렇지 않으면 false
*/
public boolean isLineEmpty(int y) {
if (y < 0 || y >= height) {
return false;
}
for (int x = 0; x < width; x++) {
if (matrix[y][x] != null) {
return false;
}
}
return true;
}
/**
* 지정된 라인이 가득 찼는지 확인합니다. 즉, 빈 스퀘어가 없는 경우입니다.
* 라인이 보드의 바깥에 있는 경우는 항상 true가 반환됩니다.
*
* @param y 세로 위치 (0 <= y < height)
* @return 전체 라인이 가득 찬 경우 true, 그렇지 않으면 false
*/
public boolean isLineFull(int y) {
if (y < 0 || y >= height) {
return true;
}
for (int x = 0; x < width; x++) {
if (matrix[y][x] == null) {
return false;
}
}
return true;
}
/**
* 보드에 가득 찬 라인이 있는지 확인합니다.
*
* @return 보드에 가득 찬 라인이 있는 경우 true, 그렇지 않으면 false
*/
public boolean hasFullLines() {
for (int y = height - 1; y >= 0; y--) {
if (isLineFull(y)) {
return true;
}
}
return false;
}
/**
* 보드를 그리기 위한 그래픽 컴포넌트를 반환합니다. 이 메소드를
* 여러 번 호출하면 동일한 컴포넌트가 반환됩니다. 스퀘어 보드는
* 하나의 그래픽 표현만 가질 수 있기 때문입니다.
*
* @return 이 보드를 그리는 그래픽 컴포넌트
*/
public Component getComponent() {
return component;
}
/**
* 보드의 높이(스퀘어 수)를 반환합니다. 이 메소드는 보드에
* 맞는 수직 스퀘어의 개수를
* <p>
* 반환합니다.
*
* @return 보드의 높이 (스퀘어 수)
*/
public int getBoardHeight() {
return height;
}
/**
* 보드의 너비(스퀘어 수)를 반환합니다. 이 메소드는 보드에
* 맞는 수평 스퀘어의 개수를 반환합니다.
*
* @return 보드의 너비 (스퀘어 수)
*/
public int getBoardWidth() {
return width;
}
/**
* 마지막 clear() 호출 이후에 제거된 라인 수를 반환합니다.
*
* @return 마지막 clear 호출 이후에 제거된 라인 수
*/
public int getRemovedLines() {
return removedLines;
}
/**
* 보드의 개별 스퀘어의 색상을 반환합니다. 스퀘어가 비어있거나
* 보드 밖에 있는 경우 null이 반환됩니다.
*
* @param x 가로 위치 (0 <= x < width)
* @param y 세로 위치 (0 <= y < height)
* @return 스퀘어 색상 또는 null (없음)
*/
public Color getSquareColor(int x, int y) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return null;
} else {
return matrix[y][x];
}
}
/**
* 보드의 개별 스퀘어의 색상을 변경합니다. 스퀘어는 다시 그려져야
* 함을 표시하기 위해 invalidateSquare() 메소드가 호출되지만,
* 그래픽 컴포넌트는 update() 메소드가 호출될 때까지 다시 그려지지
* 않습니다.
*
* @param x 가로 위치 (0 <= x < width)
* @param y 세로 위치 (0 <= y < height)
* @param color 새로운 스퀘어 색상 또는 비어있음(null)
*/
public void setSquareColor(int x, int y, Color color) {
if (x < 0 || x >= width || y < 0 || y >= height) {
return;
}
matrix[y][x] = color;
if (component != null) {
component.invalidateSquare(x, y);
}
}
/**
* 스퀘어 보드에 표시할 메시지를 설정합니다. 이는 보드가 활성화된 그림으로
* 사용되지 않을 때 사용해야 하며, 그렇지 않으면 그리기 속도가 크게 느려집니다.
*
* @param message 표시할 메시지 또는 이전 메시지를 제거하려면 null
*/
public void setMessage(String message) {
this.message = message;
if (component != null) {
component.redrawAll();
}
}
/**
* 보드를 지웁니다. 즉, 모든 색상이
* <p>
* 지워집니다. 부작용으로
* 제거된 라인 수가 0으로 재설정되고, 컴포넌트가 즉시 다시 그려집니다.
*/
public void clear() {
removedLines = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
this.matrix[y][x] = null;
}
}
if (component != null) {
component.redrawAll();
}
}
/**
* 모든 가득 찬 라인을 제거합니다. 제거된 라인 위의 모든 라인이
* 한 단계 아래로 이동되고, 맨 위에 새로운 빈 라인이 추가됩니다.
* 모든 가득 찬 라인을 제거한 후에 컴포넌트가 다시 그려집니다.
*
* @see #hasFullLines
*/
public void removeFullLines() {
boolean repaint = false;
// 가득 찬 라인 제거
for (int y = height - 1; y >= 0; y--) {
if (isLineFull(y)) {
removeLine(y);
removedLines++;
repaint = true;
y++;
}
}
// 필요한 경우 다시 그리기
if (repaint && component != null) {
component.redrawAll();
}
}
/**
* 단일 라인을 제거합니다. 위의 모든 라인이 한 단계 아래로 이동되고,
* 맨 위에 새로운 빈 라인이 추가됩니다. 라인을 제거한 후에는
* 다시 그리지 않습니다.
*
* @param y 세로 위치 (0 <= y < height)
*/
private void removeLine(int y) {
if (y < 0 || y >= height) {
return;
}
for (; y > 0; y--) {
for (int x = 0; x < width; x++) {
matrix[y][x] = matrix[y - 1][x];
}
}
for (int x = 0; x < width; x++) {
matrix[0][x] = null;
}
}
/**
* 그래픽 컴포넌트를 업데이트합니다. 이 메소드는 이전에 변경된
* 스퀘어를 다시 그립니다.
*/
public void update() {
component.redraw();
}
/**
* 정사각형 보드를 그리는 그래픽 컴포넌트입니다. 이는 내부 클래스로 구현되어 있어
* 보드 사이의 자세한 정보를 더 잘 추상화하기 위해 구현되었습니다.
*/
private class SquareBoardComponent extends JComponent {
/**
* 컴포넌트의 크기입니다. 컴포넌트가 크기가 조정된 경우에는
* paint 메서드가 실행될 때 감지됩니다. 이 값이 null로 설정되면
* 컴포넌트의 크기가 알려지지 않은 것입니다.
*/
private Dimension size = null;
/**
* 컴포넌트의 여백입니다. 여백 값은 왜곡된 가로세로비를 보정하기 위해
* 보드 주위에 테두리를 생성하는 데 사용됩니다. 컴포넌트의 크기가 조정된 경우,
* 여백 값은 paint 메서드가 실행될 때 재계산됩니다.
*/
private Insets insets = new Insets(0, 0, 0, 0);
/**
* 픽셀 단위의 정사각형 크기입니다. 이 값은
* 컴포넌트의 크기가 변경될 때, 즉 <code>size</code>
* 변수가 수정될 때 업데이트됩니다.
*/
private Dimension squareSize = new Dimension(0, 0);
/**
* 더블 버퍼링에 사용되는 이미지입니다. 보드는 먼저
* 이 이미지에 그려지고, 그 이미지가 실제 표면에 그려집니다.
* 사용자에게 그리기 과정을 보이지 않게 하기 위해
* 이 이미지는 컴포넌트 크기가 변경될 때마다 다시 생성됩니다.
*/
private Image bufferImage = null;
/**
* 클립 경계 버퍼 사각형입니다. 이 사각형은
* 클립 경계를 계산할 때 사용되며, 각 보드 사각형마다
* 새로운 클립 사각형을 할당하는 것을 피하기 위해 사용됩니다.
*/
private Rectangle bufferRect = new Rectangle();
/**
* 보드 메시지 색상입니다.
*/
private Color messageColor = Color.white;
/**
* 색상의 밝은 버전을 포함하는 룩업 테이블입니다.
* 이 테이블은 각각의 사각형이 그려질 때마다
* 색상의 밝은 버전을 계산하는 것을 피하기 위해 사용됩니다.
*/
private Hashtable lighterColors = new Hashtable();
/**
* 색상의 어두운 버전을 포함
하는 룩업 테이블입니다.
* 이 테이블은 각각의 사각형이 그려질 때마다
* 색상의 어두운 버전을 계산하는 것을 피하기 위해 사용됩니다.
*/
private Hashtable darkerColors = new Hashtable();
/**
* 컴포넌트가 업데이트되었음을 나타내는 플래그입니다.
*/
private boolean updated = true;
/**
* 업데이트해야 할 사각형의 경계 상자. 사각형 안의 좌표는
* 사각형 행렬을 참조합니다.
*/
private Rectangle updateRect = new Rectangle();
/**
* 새로운 정사각형 보드 컴포넌트를 생성합니다.
*/
public SquareBoardComponent() {
setBackground(Configuration.getColor("board.background",
"#000000"));
messageColor = Configuration.getColor("board.message",
"#ffffff");
}
/**
* 다시 그려야 할 사각형 세트에 사각형을 추가합니다.
*
* @param x 가로 위치 (0 <= x < width)
* @param y 세로 위치 (0 <= y < height)
*/
public void invalidateSquare(int x, int y) {
if (updated) {
updated = false;
updateRect.x = x;
updateRect.y = y;
updateRect.width = 0;
updateRect.height = 0;
} else {
if (x < updateRect.x) {
updateRect.width += updateRect.x - x;
updateRect.x = x;
} else if (x > updateRect.x + updateRect.width) {
updateRect.width = x - updateRect.x;
}
if (y < updateRect.y) {
updateRect.height += updateRect.y - y;
updateRect.y = y;
} else if (y > updateRect.y + updateRect.height) {
updateRect.height = y - updateRect.y;
}
}
}
/**
* 모든 무효화된 사각형을 다시 그립니다. 다시 그려야 할 사각형이
* 표시되지 않은 경우 다시 그리기가 발생하지 않습니다.
*/
public void redraw() {
Graphics g;
if (!updated) {
updated = true;
g = getGraphics();
if (g == null) return;
g.setClip(insets.left + updateRect.x * squareSize.width,
insets.top + updateRect.y * squareSize.height,
(updateRect.width + 1) * squareSize.width,
(updateRect.height + 1) * squareSize.height);
paint(g);
}
}
/**
* 컴포넌트 전체를 다시 그립니다.
*/
public void redrawAll() {
Graphics g;
updated = true;
g = getGraphics();
if (g == null) return;
g.setClip(insets.left,
insets.top,
width * squareSize.width,
height * squareSize.height);
paint(g);
}
/**
* 이 컴포넌트가 더블
버퍼링을 사용하기 때문에 true를 반환합니다.
*
* @return 더블 버퍼링을 사용하기 때문에 true를 반환합니다.
*/
public boolean isDoubleBuffered() {
return true;
}
/**
* 이 컴포넌트의 선호 크기를 반환합니다.
*
* @return 선호하는 컴포넌트 크기
*/
public Dimension getPreferredSize() {
return new Dimension(width * 20, height * 20);
}
/**
* 이 컴포넌트의 최소 크기를 반환합니다.
*
* @return 최소 컴포넌트 크기
*/
public Dimension getMinimumSize() {
return getPreferredSize();
}
/**
* 이 컴포넌트의 최대 크기를 반환합니다.
*
* @return 최대 컴포넌트 크기
*/
public Dimension getMaximumSize() {
return getPreferredSize();
}
/**
* 지정된 색상의 밝은 버전을 반환합니다. 밝은 색상은
* 해시 테이블에서 찾아보고, 이 메서드는 빠르게 실행됩니다.
* 색상이 발견되지 않으면 밝은 색상이 계산되어
* 차후에 사용하기 위해 룩업 테이블에 추가됩니다.
*
* @param c 기본 색상
* @return 색상의 밝은 버전
*/
private Color getLighterColor(Color c) {
Color lighter;
lighter = (Color) lighterColors.get(c);
if (lighter == null) {
lighter = c.brighter().brighter();
lighterColors.put(c, lighter);
}
return lighter;
}
/**
* 지정된 색상의 어두운 버전을 반환합니다. 어두운 색상은
* 해시 테이블에서 찾아보고, 이 메서드는 빠르게 실행됩니다.
* 색상이 발견되지 않으면 어두운 색상이 계산되어
* 차후에 사용하기 위해 룩업 테이블에 추가됩니다.
*
* @param c 기본 색상
* @return 색상의 어두운 버전
*/
private Color getDarkerColor(Color c) {
Color darker;
darker = (Color) darkerColors.get(c);
if (darker == null) {
darker = c.darker().darker();
darkerColors.put(c, darker);
}
return darker;
}
/**
* 이 컴포넌트를 간접적으로 그립니다. 그림은 먼저
* 버퍼 이미지에 그려지고, 그 이미지가 지정된 그래픽
* 컨텍스트에 직접 그려집니다.
*
* @param g 사용할 그
래픽 컨텍스트
*/
public synchronized void paint(Graphics g) {
Graphics bufferGraphics;
Rectangle rect;
// 컴포넌트 크기 변경 처리
if (size == null || !size.equals(getSize())) {
size = getSize();
squareSize.width = size.width / width;
squareSize.height = size.height / height;
//if (squareSize.width <= squareSize.height) {
// squareSize.height = squareSize.width;
//} else {
// squareSize.width = squareSize.height;
//}
insets.left = (size.width - width * squareSize.width) / 2;
insets.right = insets.left;
insets.top = 0;
insets.bottom = size.height - height * squareSize.height;
bufferImage = createImage(width * squareSize.width,
height * squareSize.height);
}
// 버퍼 이미지에 컴포넌트 그리기
rect = g.getClipBounds();
bufferGraphics = bufferImage.getGraphics();
bufferGraphics.setClip(rect.x - insets.left,
rect.y - insets.top,
rect.width,
rect.height);
doPaintComponent(bufferGraphics);
// 이미지 버퍼 그리기
g.drawImage(bufferImage,
insets.left,
insets.top,
getBackground(),
null);
}
/**
* 이 컴포넌트를 직접 그립니다. 보드의 모든 사각형이
* 지정된 그래픽 컨텍스트에 직접 그려집니다.
*
* @param g 사용할 그래픽 컨텍스트
*/
private void doPaintComponent(Graphics g) {
// 배경 그리기
g.setColor(getBackground());
g.fillRect(0,
0,
width * squareSize.width,
height * squareSize.height);
// 사각형 그리기
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (matrix[y][x] != null) {
paintSquare(g, x, y);
}
}
}
// 메시지 그리기
if (message != null) {
paintMessage(g, message);
}
}
/**
* 하나의 보드 사각형을 그립니다. 지정된 위치에는
* 색상 객체가 있어야 합니다.
*
* @param g 그래픽 컨텍스트를 사용합니다.
* @param x 가로 위치 (0 <= x < width)
* @param y 세로 위치 (0 <= y < height)
*/
private void paintSquare(Graphics g, int x, int y) {
Color color = matrix[y][x];
int xMin = x * squareSize.width;
int yMin = y * squareSize.height;
int xMax = xMin + squareSize.width - 1;
int yMax = yMin + squareSize.height - 1;
int i;
// 보이지 않는 경우 그리기 건너뛰기
bufferRect.x = xMin;
bufferRect.y = yMin;
bufferRect.width = squareSize.width;
bufferRect.height = squareSize.height;
if (!bufferRect.intersects(g.getClipBounds())) {
return;
}
// 기본 색상으로 채우기
g.setColor(color);
g.fillRect(xMin, yMin, squareSize.width, squareSize.height);
// 밝은 선 그리기
g.setColor(getLighterColor(color));
for (i = 0; i < squareSize.width / 10; i++) {
g.drawLine(xMin + i, yMin + i, xMax - i, yMin + i);
g.drawLine(xMin + i, yMin + i, xMin + i, yMax - i);
}
// 어두운 선 그리기
g.setColor(getDarkerColor(color));
for (i = 0; i < squareSize.width / 10; i++) {
g.drawLine(xMax - i, yMin + i, xMax - i, yMax - i);
g.drawLine(xMin + i, yMax - i, xMax - i, yMax - i);
}
}
/**
* 보드 메시지를 그립니다. 메시지는 컴포넌트의 중앙에 그려집니다.
*
* @param g 사용할 그래픽 컨텍스트
* @param msg 문자열 메시지
*/
private void paintMessage(Graphics g, String msg) {
int fontWidth;
int offset;
int x;
int y;
// 문자열 글꼴 폭 찾기
g.setFont(new Font("SansSerif", Font.BOLD, squareSize.width + 4));
fontWidth = g.getFontMetrics().stringWidth(msg);
// 중앙 위치 찾기
x = (width * squareSize.width - fontWidth) / 2;
y = height * squareSize.height / 2;
// 문자열의 검은 버전 그리기
offset = squareSize.width / 10;
g.setColor(Color.black);
g.drawString(msg, x - offset, y - offset);
g.drawString(msg, x - offset, y);
g.drawString(msg, x - offset, y - offset);
g.drawString(msg, x, y - offset);
g.drawString(msg, x, y + offset);
g.drawString(msg, x + offset, y - offset);
g.drawString(msg, x + offset, y);
g.drawString(msg, x + offset, y + offset);
// 문자열의 흰색 버전 그리기
g.setColor(messageColor);
g.drawString(msg, x, y);
}
}
}
'Java' 카테고리의 다른 글
[Java] Netty로 WebSocket 통신하기 (0) | 2023.10.02 |
---|---|
[JAVA] Swing을 이용한 스케치 기능 구현 (0) | 2023.09.12 |
[Java] awt 테트리스 모듈화 - Game (0) | 2023.07.26 |
[Java] Thread와 Swing으로 디지털 시계 만들기 (0) | 2023.07.16 |
[Java] awt 테트리스 모듈화 - Configuration (0) | 2023.07.02 |