[ft_transcendence] 3. 반응형 게임 컨테이너 제작
게임의 요구 사항에는 다음과 같은 애매한 말이 적혀있었다.
- The game must be responsive!
이게 게임이 실시간으로 반응을 해야한다는 뜻인지, 아니면 브라우저 창의 크기가 변하는 것에 따라 게임 창의 크기가 변해야 한다는 뜻인지 알 수가 없었다. 어떻게 할 지 고민하다가 최대한 보수적으로 기준을 잡고 게임 창의 크기가 브라우저 사이즈에 따라 변할 수 있도록 제작하기로 결정하였다.
게임은 html canvas를 이용해 구현되어 있었기 때문에 반응형으로 만들어주기 위해서는 브라우저 크기가 변할 시 canvas를 다시 그려주는 작업이 필요했다. 혹시나 반응형 canvas를 쉽게 그릴 수 있게 해주는 패키지가 있나 찾아보았지만, 있다 하더라도 내가 원하는 수준의 커스텀이 불가능해서 그냥 직접 구현하기로 했다.
먼저 브라우저의 사이즈가 변하는 것을 감지하고, 변한 이후 창의 크기를 가져오는 작업이 필요했다.
브라우저 사이즈가 변하는 것은 resize라는 이벤트 리스너를 등록하여 감지할 수 있었다.
// Game.tsx
// 윈도우 리사이즈 시 게임 크기 조정
useEffect(() => {
window.addEventListener('resize', adjustGameSize);
return () => {
window.removeEventListener('resize', adjustGameSize);
};
}, []);
resize 이벤트 리스너를 등록하여 브라우저의 사이즈가 변할 때 마다 게임이 렌더링 되고있는 컴포넌트의 크기를 조정해주는 함수를 실행하도록 하였다.
여기서 발생했던 문제는 canvas는 react와 무관한 html 요소로, 컴포넌트의 css가 반응형으로 지정되어있다고 해서 canvas의 크기까지 컴포넌트의 크기가 변함에 따라 자동으로 바뀌는 것은 아니라는 점이었다. 즉, 현재 변한 컴포넌트의 크기를 가져와서 해당 사이즈로 canvas를 다시 그려주는 작업이 필요했다.
이를 위해서 canvas를 감싸는 div를 ref로 등록해두고, resize event가 발생할 때 마다 div의 크기를 가져와 canvas의 크기를 갱신해주기로 했다.
// Game.tsx
const Game = () => {
// ref의 선언
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
// 이 부분에서 ref를 이용하여 canvas의 크기를 갱신한 후 다시 렌더한다.
const adjustGameSize = () => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (container && canvas) {
canvas.height = container.clientHeight;
canvas.width = container.clientWidth;
}
pongRef.current?.adjustAfterResize();
pongRef.current?.render();
};
// 컴포넌트의 렌더는 이런식으로.
return (
<div ref={containerRef} className="h-full w-full">
<canvas
className="outline-none"
tabIndex={0}
ref={canvasRef}
onKeyDown={(event) => handleKeyPress(event, 'keyDown')}
onKeyUp={(event) => handleKeyPress(event, 'keyUp')}
/>
</div>
);
}
그런데 이 때, 캔버스의 크기 뿐만 아니라 캔버스 내부에 있는 패들, 공, 네트 등의 게임 구성요소 캔버스의 크기가 변함에 따라 바뀌어야 했다. 따라서 캔버스의 크기를 갱신한 후 adjustAfterResize라는 pong 모델에 있는 함수를 호출해 캔버스 구성요소의 크기를 바꿔주었다.
서버에서 게임은 800 * 600 사이즈의 화면에서 작동하는 것으로 계산되고 있었고, 렌더 데이터들도 모두 이 기준에 맞춰져서 프론트 쪽으로 날라오고 있었다. 따라서 현재 캔버스의 크기에 맞춰서 모든 구성요소의 사이즈를 조정하는 함수들을 만들어 주었다.
// Pong.ts (캔버스를 조작하는 모델)
// 800에 대한 현재 캔버스의 x축 길이의 비를 곱하여 리턴하는 함수.
protected relativeXValue(value: number) {
const originalCanvasWidth = 800;
const currentCanvasWidth = this.canvasContext.canvas.width;
const ratio = currentCanvasWidth / originalCanvasWidth;
return value * ratio;
}
// 600에 대한 현재 캔버스의 y축 길이의 비를 곱하여 리턴하는 함수.
protected relativeYValue(value: number) {
const originalCanvasHeight = 600;
const currentCanvasHeight = this.canvasContext.canvas.height;
const ratio = currentCanvasHeight / originalCanvasHeight;
return value * ratio;
}
// 대각선 길이의 비를 곱하여 리턴하는 함수.
protected relativeDiagonalValue(value: number) {
const originalDiagonal = Math.sqrt(800 * 800 + 600 * 600);
const currentCanvasH = this.canvasContext.canvas.height;
const currentCanvasW = this.canvasContext.canvas.width;
const currentDiagonal = Math.sqrt(
currentCanvasH * currentCanvasH + currentCanvasW * currentCanvasW,
);
const ratio = currentDiagonal / originalDiagonal;
return value * ratio;
}
// 이런식으로 모든 구성요소의 사이즈를 재조정 해줬다.
public adjustAfterResize() {
const canvasWidth = this.canvasContext.canvas.width;
const canvasHeight = this.canvasContext.canvas.height;
this.p1.width = this.relativeXValue(10);
this.p1.height = this.relativeYValue(100);
this.p2.width = this.relativeXValue(10);
this.p2.height = this.relativeYValue(100);
this.p2.xPosition = canvasWidth - this.relativeXValue(10);
this.ball.radius = this.relativeDiagonalValue(10);
this.net = {
...this.net,
xPosition: (canvasWidth - 4) / 2,
width: this.relativeXValue(4),
height: this.relativeYValue(10),
};
}
반응형 게임을 구현하면서 커스텀 훅을 제대로 사용하지 못한 건 아직도 큰 아쉬움으로 남아있다.
지금 글을 쓰며 까보니 진짜 아무것도 모르고 만들었다는게 새삼 느껴지는 코드들..
개발서버를 다시 열어주신다면 리팩토링을 해서 테스트해보고싶은데.. ㅠㅠ