Search
Duplicate
🌓

Pong Game 충돌 로직에 대한 고민

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
webGL
TypeScript
수학
Scrap
태그
9 more properties

개요

42과제인 transendence를 진행하면서 구현한 Pong game을 정리해보려한다. 간단한 게임이지만 생각보다 고려해야할 지점이 몇 가지 있었고 주로 고민했던 포인트들은 다음과 같다.
정확한 공의 충돌 지점 파악
연산 속도에 따른 프레임 차이
연산 속도에 따른 터널링 발생
굉장히 빠른 속도로 공이 움직일 경우 처리
지금 생각해보면, 전부 간단하게 벡터간의 충돌을 계산함으로써 쉽게 해결할 수 있는 문제들이긴 했다.

기존 방법

처음 pong 게임을 구현한 방식은 ball의 위치, 방향, 크기를 각각 정의하고 한번의 연산마다 ball의 크기를 조정하여, paddle의 위치를 기반으로 충돌을 계산하는 방식을 사용했었다.
이때 연산되는 시간이 일정하지 않을 경우, 공이 빨라지거나 느려지는 현상이 있었다.
또한 충돌을 계산하는 방식이 한번에 연산에 이루어지다 보니, 공이 다음 연산에서 패들을 넘어갈 경우 이를 감지하지 못하는 일이 발생했다. 공이 빠른 속도로 움직이거나, 연산 사이의 과정이 너무 길 경우 해당 문제들이 일어났다.
마지막으로 충돌이 일어날 때, 공이 패들을 조금 파고들어간 후에, 충돌을 감지하는 일이 빈번하게 일어났다. 그렇다고 충돌을 미리 감지하고 방향을 바꾸기에는 패들에 닿지 않았는데도 공이 튕겨지는 듯한 연산이 일어났다.

해결방법

각 연산을 일정하게 보장하기 위해서 delta값을 정의하고 해당 값을 각각의 크기에 곱해줌으로써 일정한 속도를 보장했다.
예시:
/* delta 값 계산*/ { ... let delta = (timeStamp - data.lastTime) / 1000.0; update(delta); render(); data.lastTime = timeStamp; } /* 공의 움직임 */ static calculateBallPosition(ball: Ball, delta: number, factor: number) : vec2 { let tempVec2 = vec2.create(); vec2.add(tempVec2, ball.position, vec2.scale(tempVec2, ball.direction, ball.velocity * delta)); return tempVec2; } /* 패들의 움직임 */ if (data.keyPress.up) { paddle[0].position[1] += paddle[0].paddleSpeed * delta } else if (data.keyPress.down) { paddle[0].position[1] -= paddle[0].paddleSpeed * delta; } else { paddle[0].position[1] += 0; }
TypeScript
복사
충돌 로직의 불완전함을 보완하기 위해서 두 벡터간의 충돌을 계산하는 식을 사용했다. 전자를 공에 대한 벡터의 방정식, 후자를 패들에 대한 방정식으로 설정하고, 이를 풀어서 계산해보면
a+pb=c+qda + p * b = c + q * d
p=(ca)×db×d p = \frac{(c - a) \times d}{b \times d} 
q=(ac)×bd×b q = \frac{(a - c) \times b}{d \times b} 
예시:
private static crossProduct = (a: vec2, b: vec2): number => a[0] * b[1] - a[1] * b[0]; private static calculateConflict(a: vec2, b: vec2, c : vec2, d :vec2) { const crossB_D = this.crossProduct(b, d); if (crossB_D === 0) { return { p: -1, q: -1 }; } const p = (this.crossProduct(vec2.sub(vec2.create(), c, a), d)) / this.crossProduct(b, d); const q = (this.crossProduct(vec2.sub(vec2.create(), a, c), b)) / this.crossProduct(d, b); return {p, q}; } private static checkConflict(p: number, q: number, l: number, delta: number) : boolean { // l은 패들의 길이를 나타낸다. return (q < 0 || q > l) || p > delta || p < 0; }
TypeScript
복사
최종적으로 위 두 식을 얻을 수 있는데, 이때 p는 충돌까지 필요한 연산량 즉, 시간을 의미하고, q는 충돌에 필요한 패들의 범위를 나타내게 된다. 이렇게 얻은 값을 토대로 충돌 시간을 계산한다. 이때, delta값과 이를 비교해서 delta 값보다 p가 더 크다면 충돌하지 않은 것으로, 작다면 충돌한 것으로 인식해서 처리하게 된다. 그렇게 충돌을 진행시킨 후에, 남은 delta - p 만큼의 추가 연산을 진행하여 충돌을 보장한다.
중요한 것은 delta - p를 가지고 연산 할 때, 재귀적으로 다시 충돌을 감지해야한다. delta 값이 지나치게 클 경우나 속도가 너무 빠를 경우 한번의 연산 안에서 두 번 이상의 충돌이 발생할 수 있기 때문이다.
예시:
static GuaranteeConflict(ball: Ball, delta: number) { const p = this.calCheckConflict(ball, delta, data.paddle); if (p === -1) { this.updateBallPosition(delta); return ; } this.updateBallPosition(p.p); this.handleBallPaddleCollision(); const restAfterCollision = delta - p.p; this._paddlePos = p.pos; this.GuaranteeConflict(ball, restAfterCollision); }
TypeScript
복사
여기서 눈여겨 볼 점은 충돌이 없을 때, 까지 재귀적으로 호출된다는 점이다.